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:
- Grouped barplots (
ascii_plot_bar) for categorical variables - Grouped histograms (
ascii_plot_hist) for numeric variables - Comparative histograms (
ascii_comparative_hist) with baseline-relative rendering ascii_plot_distβ the all-in-one dispatcher- Using
library="balance"with the.covars().plot()API - 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.
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)
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.
# 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.
# 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.
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.
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
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.
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.
# 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
# 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.