ASCII Plots TutorialΒΆ

This tutorial demonstrates the text-based plotting functions in balance. ASCII plots are useful in terminals, CI logs, notebooks with limited rendering, or anywhere you want a quick visual comparison without a graphical backend.

We cover:

  1. Grouped barplots (ascii_plot_bar) for categorical variables
  2. Grouped histograms (ascii_plot_hist) for numeric variables
  3. Comparative histograms (ascii_comparative_hist) with baseline-relative rendering
  4. ascii_plot_dist β€” the all-in-one dispatcher
  5. Using library="balance" with the .covars().plot() API
  6. Switching to grouped-bar histograms with comparative=False

SetupΒΆ

Load the toy dataset and run an IPW adjustment so we have three datasets to compare: unadjusted sample, adjusted sample, and target population.

InΒ [1]:
import warnings
warnings.filterwarnings("ignore")

from balance import Sample, load_data

target_df, sample_df = load_data()

sample = Sample.from_frame(sample_df, outcome_columns=["happiness"])
target = Sample.from_frame(target_df, outcome_columns=["happiness"])
sample_with_target = sample.set_target(target)
adjusted = sample_with_target.adjust()
INFO (2026-04-19 15:14:27,504) [__init__/<module> (line 75)]: Using balance version 0.19.0
INFO (2026-04-19 15:14:27,505) [__init__/<module> (line 80)]: 
balance (Version 0.19.0) loaded:
    πŸ“– Documentation: https://import-balance.org/
    πŸ› οΈ Help / Issues: https://github.com/facebookresearch/balance/issues/
    πŸ“„ Citation:
        Sarig, T., Galili, T., & Eilat, R. (2023).
        balance - a Python package for balancing biased data samples.
        https://arxiv.org/abs/2307.06024

    Tip: You can view this message anytime with balance.help()

WARNING (2026-04-19 15:14:27,527) [input_validation/guess_id_column (line 336)]: Guessed id column name id for the data
WARNING (2026-04-19 15:14:27,540) [sample_frame/from_frame (line 327)]: No weights passed. Adding a 'weight' column and setting all values to 1
WARNING (2026-04-19 15:14:27,543) [input_validation/guess_id_column (line 336)]: Guessed id column name id for the data
WARNING (2026-04-19 15:14:27,557) [sample_frame/from_frame (line 327)]: No weights passed. Adding a 'weight' column and setting all values to 1
INFO (2026-04-19 15:14:27,567) [ipw/ipw (line 723)]: Starting ipw function
INFO (2026-04-19 15:14:27,569) [adjustment/apply_transformations (line 433)]: Adding the variables: []
INFO (2026-04-19 15:14:27,569) [adjustment/apply_transformations (line 434)]: Transforming the variables: ['gender', 'age_group', 'income']
INFO (2026-04-19 15:14:27,578) [adjustment/apply_transformations (line 469)]: Final variables in output: ['gender', 'age_group', 'income']
INFO (2026-04-19 15:14:27,703) [ipw/ipw (line 797)]: Building model matrix
INFO (2026-04-19 15:14:27,704) [ipw/ipw (line 798)]: The formula used to build the model matrix: ['income + gender + age_group + _is_na_gender']
INFO (2026-04-19 15:14:27,704) [ipw/ipw (line 799)]: The number of columns in the model matrix: 16
INFO (2026-04-19 15:14:27,705) [ipw/ipw (line 800)]: The number of rows in the model matrix: 11000
INFO (2026-04-19 15:14:46,049) [ipw/ipw (line 996)]: Done with sklearn
INFO (2026-04-19 15:14:46,050) [ipw/ipw (line 998)]: max_de: None
INFO (2026-04-19 15:14:46,051) [ipw/ipw (line 1020)]: Starting model selection
INFO (2026-04-19 15:14:46,054) [ipw/ipw (line 1075)]: Chosen lambda: 0.041158338186664825
INFO (2026-04-19 15:14:46,055) [ipw/ipw (line 1092)]: Proportion null deviance explained 0.172637976731583

1. All covariates at a glanceΒΆ

The easiest way to get an ASCII comparison is through the standard .covars().plot() API with library="balance". This automatically classifies each variable as categorical or numeric and renders grouped barplots or histograms accordingly.

The three fill characters distinguish the datasets:

  • β–ˆ = sample (unadjusted)
  • β–’ = adjusted (after IPW)
  • ▐ = population (target)
InΒ [2]:
adjusted.covars().plot(library="balance");
=== gender (categorical) ===

Category | population  adjusted  sample
         |
Female   | β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ (50.0%)
         | β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’ (44.5%)
         | ▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐ (29.4%)

Male     | β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ (50.0%)
         | β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’ (55.5%)
         | ▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐ (70.6%)

Legend: β–ˆ population  β–’ adjusted  ▐ sample
Bar lengths are proportional to weighted frequency within each dataset.

=== age_group (categorical) ===

Category | population  adjusted  sample
         |
18-24    | β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ (19.7%)
         | β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’ (28.1%)
         | ▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐ (49.1%)

25-34    | β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ (29.7%)
         | β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’ (30.7%)
         | ▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐ (30.0%)

35-44    | β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ (29.9%)
         | β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’ (27.4%)
         | ▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐ (15.6%)

45+      | β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ (20.6%)
         | β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’ (13.8%)
         | ▐▐▐▐▐▐ (5.3%)

Legend: β–ˆ population  β–’ adjusted  ▐ sample
Bar lengths are proportional to weighted frequency within each dataset.

=== income (numeric, comparative) ===

Range            | population (%) | adjusted (%)   | sample (%)       
----------------------------------------------------------------------
[0.00, 8.57)     | β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ 49.0  | β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–’ 54.8 | β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–’β–’β–’β–’ 73.2
[8.57, 17.14)    | β–ˆβ–ˆβ–ˆβ–ˆ 23.1      | β–ˆβ–ˆβ–ˆβ–ˆ 26.3      | β–ˆβ–ˆβ–ˆ] 19.2        
[17.14, 25.71)   | β–ˆβ–ˆ 13.2        | β–ˆβ–ˆ 12.3        | β–ˆ] 5.3           
[25.71, 34.28)   | β–ˆ 7.3          | β–ˆ 3.9          | ] 1.6            
[34.28, 42.85)   | β–ˆ 3.9          | ] 1.5          | ] 0.4            
[42.85, 51.41)   | 1.8            | 0.2            | 0.1              
[51.41, 59.98)   | 0.9            | 1.0            | 0.2              
[59.98, 68.55)   | 0.4            | 0.0            | 0.0              
[68.55, 77.12)   | 0.2            | 0.0            | 0.0              
[77.12, 85.69)   | 0.1            | 0.0            | 0.0              
[85.69, 94.26)   | 0.0            | 0.0            | 0.0              
[94.26, 102.83)  | 0.0            | 0.0            | 0.0              
[102.83, 111.40) | 0.0            | 0.0            | 0.0              
[111.40, 119.97) | 0.0            | 0.0            | 0.0              
[119.97, 128.54] | 0.0            | 0.0            | 0.0              
----------------------------------------------------------------------
Total            | 100.0          | 100.0          | 100.0            

Key: β–ˆ = shared with population, β–’ = excess,    ] = deficit

2. Category spacingΒΆ

By default, barplots insert a blank line between categories for readability. You can disable this with separate_categories=False to get a more compact view.

InΒ [3]:
# Compact view (no blank lines between categories)
adjusted.covars().plot(
    library="balance", variables=["gender"],
    separate_categories=False,
);
=== gender (categorical) ===

Category | population  adjusted  sample
         |
Female   | β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ (50.0%)
         | β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’ (44.5%)
         | ▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐ (29.4%)
Male     | β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ (50.0%)
         | β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’ (55.5%)
         | ▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐ (70.6%)

Legend: β–ˆ population  β–’ adjusted  ▐ sample
Bar lengths are proportional to weighted frequency within each dataset.

InΒ [4]:
# Default view (blank lines between categories)
adjusted.covars().plot(
    library="balance", variables=["gender"],
);
=== gender (categorical) ===

Category | population  adjusted  sample
         |
Female   | β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ (50.0%)
         | β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’ (44.5%)
         | ▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐ (29.4%)

Male     | β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ (50.0%)
         | β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’ (55.5%)
         | ▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐ (70.6%)

Legend: β–ˆ population  β–’ adjusted  ▐ sample
Bar lengths are proportional to weighted frequency within each dataset.

3. Focusing on a single variableΒΆ

Pass variables=["income"] to plot just one covariate. For numeric variables the output is a grouped histogram where each bin shows the weighted proportion for the unadjusted sample, adjusted sample, and target population.

InΒ [5]:
adjusted.covars().plot(
    library="balance", variables=["income"],
);
=== income (numeric, comparative) ===

Range            | population (%) | adjusted (%)   | sample (%)       
----------------------------------------------------------------------
[0.00, 8.57)     | β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ 49.0  | β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–’ 54.8 | β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–’β–’β–’β–’ 73.2
[8.57, 17.14)    | β–ˆβ–ˆβ–ˆβ–ˆ 23.1      | β–ˆβ–ˆβ–ˆβ–ˆ 26.3      | β–ˆβ–ˆβ–ˆ] 19.2        
[17.14, 25.71)   | β–ˆβ–ˆ 13.2        | β–ˆβ–ˆ 12.3        | β–ˆ] 5.3           
[25.71, 34.28)   | β–ˆ 7.3          | β–ˆ 3.9          | ] 1.6            
[34.28, 42.85)   | β–ˆ 3.9          | ] 1.5          | ] 0.4            
[42.85, 51.41)   | 1.8            | 0.2            | 0.1              
[51.41, 59.98)   | 0.9            | 1.0            | 0.2              
[59.98, 68.55)   | 0.4            | 0.0            | 0.0              
[68.55, 77.12)   | 0.2            | 0.0            | 0.0              
[77.12, 85.69)   | 0.1            | 0.0            | 0.0              
[85.69, 94.26)   | 0.0            | 0.0            | 0.0              
[94.26, 102.83)  | 0.0            | 0.0            | 0.0              
[102.83, 111.40) | 0.0            | 0.0            | 0.0              
[111.40, 119.97) | 0.0            | 0.0            | 0.0              
[119.97, 128.54] | 0.0            | 0.0            | 0.0              
----------------------------------------------------------------------
Total            | 100.0          | 100.0          | 100.0            

Key: β–ˆ = shared with population, β–’ = excess,    ] = deficit

4. Grouped histogram (ascii_plot_hist)ΒΆ

ascii_plot_hist provides a side-by-side view: each dataset gets its own bar (with a distinct fill character) so you can compare how mass is distributed across bins. Bar lengths are proportional to weighted frequency within each dataset.

InΒ [6]:
from balance.stats_and_plots.ascii_plots import ascii_plot_hist

dfs = [
    {"df": target_df, "weight": None},
    {"df": sample_df, "weight": None},
]
print(ascii_plot_hist(
    dfs, names=["Target", "Sample"], column="income",
))
=== income (numeric) ===

Bin              | Target  Sample
                 |
[0.00, 8.57)     | β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ (49.0%)
                 | β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’ (73.2%)
[8.57, 17.14)    | β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ (23.1%)
                 | β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’ (19.2%)
[17.14, 25.71)   | β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ (13.2%)
                 | β–’β–’β–’β–’ (5.3%)
[25.71, 34.28)   | β–ˆβ–ˆβ–ˆβ–ˆβ–ˆ (7.3%)
                 | β–’ (1.6%)
[34.28, 42.85)   | β–ˆβ–ˆβ–ˆ (3.9%)
                 | . (0.4%)
[42.85, 51.41)   | β–ˆ (1.8%)
                 | . (0.1%)
[51.41, 59.98)   | β–ˆ (0.9%)
                 | . (0.2%)
[59.98, 68.55)   | . (0.4%)
                 |  (0.0%)
[68.55, 77.12)   | . (0.2%)
                 |  (0.0%)
[77.12, 85.69)   | . (0.1%)
                 |  (0.0%)
[85.69, 94.26)   | . (0.0%)
                 |  (0.0%)
[94.26, 102.83)  | . (0.0%)
                 |  (0.0%)
[102.83, 111.40) |  (0.0%)
                 |  (0.0%)
[111.40, 119.97) |  (0.0%)
                 |  (0.0%)
[119.97, 128.54] | . (0.0%)
                 |  (0.0%)

Legend: β–ˆ Target  β–’ Sample
Bar lengths are proportional to weighted frequency within each dataset.

5. Comparative histogram (ascii_comparative_hist)ΒΆ

When you want to see how a dataset differs from a baseline, ascii_comparative_hist renders each bin relative to the first dataset:

  • β–ˆ (solid) = the proportion shared with the baseline
  • β–’ (shaded) = excess beyond the baseline
  • ] (right bracket) = deficit relative to the baseline
InΒ [7]:
from balance.stats_and_plots.ascii_plots import ascii_comparative_hist

# Target (baseline) vs unadjusted sample
dfs = [
    {"df": target_df, "weight": None},
    {"df": sample_df, "weight": None},
]
print(ascii_comparative_hist(
    dfs, names=["Target", "Sample"], column="income",
))
=== income (numeric, comparative) ===

Range            | Target (%)           | Sample (%)                 
---------------------------------------------------------------------
[0.00, 8.57)     | β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ 49.0 | β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–’β–’β–’β–’β–’β–’β–’ 73.2
[8.57, 17.14)    | β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ 23.1         | β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ] 19.2               
[17.14, 25.71)   | β–ˆβ–ˆβ–ˆβ–ˆ 13.2            | β–ˆβ–ˆ ] 5.3                   
[25.71, 34.28)   | β–ˆβ–ˆ 7.3               |  ] 1.6                     
[34.28, 42.85)   | β–ˆ 3.9                | ] 0.4                      
[42.85, 51.41)   | β–ˆ 1.8                | ] 0.1                      
[51.41, 59.98)   | 0.9                  | 0.2                        
[59.98, 68.55)   | 0.4                  | 0.0                        
[68.55, 77.12)   | 0.2                  | 0.0                        
[77.12, 85.69)   | 0.1                  | 0.0                        
[85.69, 94.26)   | 0.0                  | 0.0                        
[94.26, 102.83)  | 0.0                  | 0.0                        
[102.83, 111.40) | 0.0                  | 0.0                        
[111.40, 119.97) | 0.0                  | 0.0                        
[119.97, 128.54] | 0.0                  | 0.0                        
---------------------------------------------------------------------
Total            | 100.0                | 100.0                      

Key: β–ˆ = shared with Target, β–’ = excess,    ] = deficit

Three-way comparative histogramΒΆ

You can also compare the target, unadjusted, and adjusted distributions side by side. Here the target is the baseline, so β–’ shows where the other datasets have more mass and ] shows where they have less.

InΒ [8]:
dfs = [
    {"df": target.covars().df, "weight": target.weight_series},
    {"df": sample_with_target.covars().df, "weight": sample_with_target.weight_series},
    {"df": adjusted.covars().df, "weight": adjusted.weight_series},
]
print(ascii_comparative_hist(
    dfs, names=["Target", "Unadjusted", "Adjusted"],
    column="income",
))
=== income (numeric, comparative) ===

Range            | Target (%)    | Unadjusted (%)    | Adjusted (%)  
---------------------------------------------------------------------
[0.00, 8.57)     | β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ 49.0 | β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–’β–’β–’β–’ 73.2 | β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–’ 54.8
[8.57, 17.14)    | β–ˆβ–ˆβ–ˆβ–ˆ 23.1     | β–ˆβ–ˆβ–ˆ] 19.2         | β–ˆβ–ˆβ–ˆβ–ˆ 26.3     
[17.14, 25.71)   | β–ˆβ–ˆ 13.2       | β–ˆ] 5.3            | β–ˆβ–ˆ 12.3       
[25.71, 34.28)   | β–ˆ 7.3         | ] 1.6             | β–ˆ 3.9         
[34.28, 42.85)   | β–ˆ 3.9         | ] 0.4             | ] 1.5         
[42.85, 51.41)   | 1.8           | 0.1               | 0.2           
[51.41, 59.98)   | 0.9           | 0.2               | 1.0           
[59.98, 68.55)   | 0.4           | 0.0               | 0.0           
[68.55, 77.12)   | 0.2           | 0.0               | 0.0           
[77.12, 85.69)   | 0.1           | 0.0               | 0.0           
[85.69, 94.26)   | 0.0           | 0.0               | 0.0           
[94.26, 102.83)  | 0.0           | 0.0               | 0.0           
[102.83, 111.40) | 0.0           | 0.0               | 0.0           
[111.40, 119.97) | 0.0           | 0.0               | 0.0           
[119.97, 128.54] | 0.0           | 0.0               | 0.0           
---------------------------------------------------------------------
Total            | 100.0         | 100.0             | 100.0         

Key: β–ˆ = shared with Target, β–’ = excess,    ] = deficit

6. Grouped histograms via comparative=FalseΒΆ

By default, ascii_plot_dist (and .covars().plot(library="balance")) renders numeric variables as comparative histograms showing excess/deficit relative to a baseline. If you prefer the simpler grouped-bar style (same layout used for categorical variables), pass comparative=False.

InΒ [9]:
# Comparative mode (default) β€” numeric variables show excess/deficit vs baseline
adjusted.covars().plot(
    library="balance", variables=["income"],
);
=== income (numeric, comparative) ===

Range            | population (%) | adjusted (%)   | sample (%)       
----------------------------------------------------------------------
[0.00, 8.57)     | β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ 49.0  | β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–’ 54.8 | β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–’β–’β–’β–’ 73.2
[8.57, 17.14)    | β–ˆβ–ˆβ–ˆβ–ˆ 23.1      | β–ˆβ–ˆβ–ˆβ–ˆ 26.3      | β–ˆβ–ˆβ–ˆ] 19.2        
[17.14, 25.71)   | β–ˆβ–ˆ 13.2        | β–ˆβ–ˆ 12.3        | β–ˆ] 5.3           
[25.71, 34.28)   | β–ˆ 7.3          | β–ˆ 3.9          | ] 1.6            
[34.28, 42.85)   | β–ˆ 3.9          | ] 1.5          | ] 0.4            
[42.85, 51.41)   | 1.8            | 0.2            | 0.1              
[51.41, 59.98)   | 0.9            | 1.0            | 0.2              
[59.98, 68.55)   | 0.4            | 0.0            | 0.0              
[68.55, 77.12)   | 0.2            | 0.0            | 0.0              
[77.12, 85.69)   | 0.1            | 0.0            | 0.0              
[85.69, 94.26)   | 0.0            | 0.0            | 0.0              
[94.26, 102.83)  | 0.0            | 0.0            | 0.0              
[102.83, 111.40) | 0.0            | 0.0            | 0.0              
[111.40, 119.97) | 0.0            | 0.0            | 0.0              
[119.97, 128.54] | 0.0            | 0.0            | 0.0              
----------------------------------------------------------------------
Total            | 100.0          | 100.0          | 100.0            

Key: β–ˆ = shared with population, β–’ = excess,    ] = deficit
InΒ [10]:
# Grouped-bar mode β€” numeric variables use the same bar style as categorical
adjusted.covars().plot(
    library="balance", variables=["income"],
    comparative=False,
);
=== income (numeric) ===

Bin              | population  adjusted  sample
                 |
[0.00, 8.57)     | β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ (49.0%)
                 | β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’ (54.8%)
                 | ▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐ (73.2%)
[8.57, 17.14)    | β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ (23.1%)
                 | β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’ (26.3%)
                 | ▐▐▐▐▐▐▐▐▐▐▐▐▐▐ (19.2%)
[17.14, 25.71)   | β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ (13.2%)
                 | β–’β–’β–’β–’β–’β–’β–’β–’β–’ (12.3%)
                 | ▐▐▐▐ (5.3%)
[25.71, 34.28)   | β–ˆβ–ˆβ–ˆβ–ˆβ–ˆ (7.3%)
                 | β–’β–’β–’ (3.9%)
                 | ▐ (1.6%)
[34.28, 42.85)   | β–ˆβ–ˆβ–ˆ (3.9%)
                 | β–’ (1.5%)
                 | . (0.4%)
[42.85, 51.41)   | β–ˆ (1.8%)
                 | . (0.2%)
                 | . (0.1%)
[51.41, 59.98)   | β–ˆ (0.9%)
                 | β–’ (1.0%)
                 | . (0.2%)
[59.98, 68.55)   | . (0.4%)
                 |  (0.0%)
                 |  (0.0%)
[68.55, 77.12)   | . (0.2%)
                 |  (0.0%)
                 |  (0.0%)
[77.12, 85.69)   | . (0.1%)
                 |  (0.0%)
                 |  (0.0%)
[85.69, 94.26)   | . (0.0%)
                 |  (0.0%)
                 |  (0.0%)
[94.26, 102.83)  | . (0.0%)
                 |  (0.0%)
                 |  (0.0%)
[102.83, 111.40) |  (0.0%)
                 |  (0.0%)
                 |  (0.0%)
[111.40, 119.97) |  (0.0%)
                 |  (0.0%)
                 |  (0.0%)
[119.97, 128.54] | . (0.0%)
                 |  (0.0%)
                 |  (0.0%)

Legend: β–ˆ population  β–’ adjusted  ▐ sample
Bar lengths are proportional to weighted frequency within each dataset.