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-03-02 10:28:40,992) [__init__/<module> (line 72)]: Using balance version 0.16.1
WARNING (2026-03-02 10:28:41,187) [input_validation/guess_id_column (line 337)]: Guessed id column name id for the data
balance (Version 0.16.1) 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-03-02 10:28:41,198) [sample_class/from_frame (line 549)]: No weights passed. Adding a 'weight' column and setting all values to 1
WARNING (2026-03-02 10:28:41,210) [input_validation/guess_id_column (line 337)]: Guessed id column name id for the data
WARNING (2026-03-02 10:28:41,225) [sample_class/from_frame (line 549)]: No weights passed. Adding a 'weight' column and setting all values to 1
INFO (2026-03-02 10:28:41,234) [ipw/ipw (line 703)]: Starting ipw function
INFO (2026-03-02 10:28:41,236) [adjustment/apply_transformations (line 433)]: Adding the variables: []
INFO (2026-03-02 10:28:41,237) [adjustment/apply_transformations (line 434)]: Transforming the variables: ['gender', 'age_group', 'income']
INFO (2026-03-02 10:28:41,246) [adjustment/apply_transformations (line 469)]: Final variables in output: ['gender', 'age_group', 'income']
INFO (2026-03-02 10:28:41,254) [ipw/ipw (line 738)]: Building model matrix
INFO (2026-03-02 10:28:41,365) [ipw/ipw (line 764)]: The formula used to build the model matrix: ['income + gender + age_group + _is_na_gender']
INFO (2026-03-02 10:28:41,365) [ipw/ipw (line 767)]: The number of columns in the model matrix: 16
INFO (2026-03-02 10:28:41,366) [ipw/ipw (line 768)]: The number of rows in the model matrix: 11000
INFO (2026-03-02 10:28:58,221) [ipw/ipw (line 990)]: Done with sklearn
INFO (2026-03-02 10:28:58,222) [ipw/ipw (line 992)]: max_de: None
INFO (2026-03-02 10:28:58,223) [ipw/ipw (line 1014)]: Starting model selection
INFO (2026-03-02 10:28:58,225) [ipw/ipw (line 1047)]: Chosen lambda: 0.041158338186664825
INFO (2026-03-02 10:28:58,226) [ipw/ipw (line 1065)]: 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_column},
    {"df": sample_with_target.covars().df, "weight": sample_with_target.weight_column},
    {"df": adjusted.covars().df, "weight": adjusted.weight_column},
]
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.