Mixed and categorical data#

Cover: an abacus seen at an angle, one bead amber, drawn in navy lines

Your tables may contain more than numbers. A customer record holds a plan and a region alongside hours used and the amount billed. A patient record holds a diagnosis alongside blood pressure. With Andrey 0.6.0, you can learn causal graphs from categorical data and from tables that mix categorical and continuous columns.

TL;DR#

  • GES on categorical data: more than 50x faster than causal-learn at every size, with the same graph on every dataset.

  • Hill climbing on categorical data: more than 20x faster than pgmpy at every size.

  • PC on mixed data: more than 10x faster than Tetrad at 10 and 20 variables.

We explain how Andrey reads each kind of column, run an example, and report benchmark results.

Tests and scores#

A constraint-based method tests whether two variables are independent given others. A score-based method compares graphs by how well they explain the data. Each needs a test or score that fits your data. In Andrey 0.6.0 you can use both with categorical and mixed data.

Data

Tests

Scores

Categorical

"chisq" (chi-square), "gsq" (G-square)

"local_score_BDeu", "local_score_BIC_categorical" (multinomial BIC)

Mixed

"degen_gauss" (degenerate Gaussian), "cond_gauss" (conditional Gaussian)

"local_score_degen_gauss", "local_score_cond_gauss"

PC, FCI, GFCI, and CD-NOD take every test. GES, GFCI, hill climbing, and exact search take every score; Tabu search takes the categorical scores, and BOSS and GRaSP the mixed ones. GFCI, CD-NOD, exact search, and the mixed scores in BOSS and GRaSP are experimental.

The two mixed-data forms make different assumptions:

  • Degenerate Gaussian replaces a categorical column of k categories with k - 1 indicator columns and treats every column as Gaussian. It suits effects that add up across categories. It is the faster of the two, though only slightly in PC.

  • Conditional Gaussian fits a Gaussian with its own mean and covariance to the continuous variables within each combination of categories. A category can change the spread as well as the average. It needs several rows in each combination.

You do not need to encode a DataFrame’s text, bool, or category columns; Andrey reads them as categorical. With a mixed-data test or score, Andrey also reads a column with at most 10 distinct whole numbers as categorical and issues a warning. To be sure, name the categorical columns with categorical=[...], or change the limit with max_categories.

Example#

We simulate 2,000 customers. Their plan and region each change how many hours they use the service. The hours set the monthly bill. Plan and region are text; hours and bill are numbers.

example.py#
import numpy as np
import pandas as pd

import andrey

# Customers of a service. Their plan and their region each change how many hours they use it,
# and the hours set the monthly bill. Plan and region are text; hours and bill are numbers.
rng = np.random.default_rng(0)
n = 2000
plan = rng.choice(["basic", "pro"], size=n)
region = rng.choice(["north", "south", "west"], size=n)
shift = {"north": 0.0, "south": 3.0, "west": -3.0}
hours = 10 + 12 * (plan == "pro") + np.vectorize(shift.get)(region) + rng.normal(0, 3, n)
bill = 5 + 1.5 * hours + rng.normal(0, 5, n)
data = pd.DataFrame({"plan": plan, "region": region, "hours": hours, "bill": bill})

# The conditional Gaussian test and score handle both kinds of column.
categorical = ["plan", "region"]
print(andrey.pc(data, indep_test="cond_gauss", categorical=categorical), end="\n\n")
print(andrey.ges(data, score_func="local_score_cond_gauss", categorical=categorical))

PC with the conditional Gaussian test and GES with the conditional Gaussian score both find and orient all three links in the model. Plan and region are independent, but both change the hours. This lets the data show which way those two links run, then orient the link from hours to the bill.

example.py#
PC  cpdag  |  4 nodes  |  3 edges (3 directed)

  plan -> hours
  region -> hours
  hours -> bill

  categorical: ['plan', 'region']

GES  cpdag  |  4 nodes  |  3 edges (3 directed)

  plan -> hours
  region -> hours
  hours -> bill

  score: 2.943e+04
  categorical: ['plan', 'region']

Benchmarks#

We compare methods on simulated categorical and conditional linear Gaussian networks with 10, 20, and 40 variables and 2,000 rows. We use 5 categorical datasets and 10 mixed datasets at each size. We compare each method with packages that implement it: causal-learn and pgmpy in Python, bnlearn in R, and Tetrad in Java.

Method

Data

Compared with

Andrey’s speedup

Graph

GES, BDeu score

Categorical

causal-learn

more than 50x at every size

the same as causal-learn’s on every dataset

Hill climbing, BDeu and BIC scores

Categorical

pgmpy

more than 20x at every size

the same scores; the graphs differ on 28 of 30 datasets, with fewer errors on 9 and more on 17; on those 17 the two searches part at a tie between two equal moves, which they break differently

PC, conditional Gaussian test

Mixed

Tetrad

more than 10x at 10 and 20 variables

fewer errors on 12 of the 18 datasets Tetrad finished, more on 4

GES, conditional Gaussian score

Mixed

Tetrad (FGES)

more than 9x at every size

the same on 11 of 30 datasets; of the other 19, fewer errors on 11 and more on 7

PC, chi-square and G-square tests

Categorical

bnlearn

more than 3x at 20 and 40 variables

fewer errors on 24 of 30 datasets, more on 2

PC, chi-square and G-square tests

Categorical

causal-learn

more than 2x at 20 and 40 variables

the same as causal-learn’s on every dataset

Each speedup holds at every size it names. At 10 variables, PC on categorical data takes under 0.1 s in every package, too fast to time fairly. Its speedups therefore start at 20. Tetrad’s PC with the conditional Gaussian test stopped with an error on 12 of its 30 fits. The table counts only the datasets both packages finished.

You can find every measurement in the 0.6.0 release card, including comparisons with bnlearn’s hill climbing and the degenerate Gaussian forms. It also shows where Andrey is not faster and gives the machine, settings, and known limits.

More features#

  • Simulated networks. andrey.data.sample_discrete and sample_mixed sample a categorical or a conditional linear Gaussian network along with its true graph.

  • Published networks as categorical data. load_dataset("alarm", discrete=True) samples ASIA, ALARM, HEPAR2, or ANDES from the network’s own probability tables.

  • Hill climbing respects the model. With a mixed-data score, hc never adds an edge from a continuous variable to a categorical one. A conditional linear Gaussian network cannot express that edge.

The categorical tests and scores build on work by Rui Zhu (#94). Thank you, Rui.

For details, read the changelog.

Try it#

pip install -U andrey-core

Pass a DataFrame with text or category columns to andrey.pc(data, indep_test="cond_gauss") or andrey.ges(data, score_func="local_score_cond_gauss"), or use "chisq" and "local_score_BDeu" when every column is categorical.

Contribute#

Andrey is in alpha. The API may change between releases. When you try it on your own data, tell us what breaks or which method you need. We also welcome pull requests. You can find setup instructions in the contributing guide.