Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Mixture Density Network (MDN) imputation

This notebook demonstrates how to use Microimpute’s MDN imputer to impute values using mixture density networks. MDN models the full conditional distribution of a target variable as a mixture of Gaussians, enabling it to capture complex, multi-modal relationships.

Variable type support

The MDN model automatically handles both numerical and categorical variables. For numerical targets, it applies a mixture density network that learns the parameters of a Gaussian mixture model. For categorical targets (strings, booleans, or numerically-encoded categorical variables), it switches to using a neural classifier. This automatic adaptation happens internally without requiring any manual configuration.

MDN class

class MDN(
    layers: str = "128-64-32",
    activation: str = "ReLU",
    dropout: float = 0.0,
    use_batch_norm: bool = False,
    num_gaussian: int = 5,
    softmax_temperature: float = 1.0,
    n_samples: int = 100,
    learning_rate: float = 1e-3,
    max_epochs: int = 100,
    early_stopping_patience: int = 10,
    batch_size: int = 256,
    model_dir: str = "./microimpute_models",
    force_retrain: bool = False,
    seed: Optional[int] = RANDOM_STATE,
    log_level: Optional[str] = "WARNING"
)
ParameterTypeDefault usedDescription
layersstr“128-64-32”Network architecture as hyphen-separated layer sizes
activationstr“ReLU”Activation function (ReLU, LeakyReLU, SELU, etc.)
dropoutfloat0.0Dropout probability for regularization
use_batch_normboolFalseWhether to use batch normalization
num_gaussianint5Number of Gaussian components in the mixture
softmax_temperaturefloat1.0Temperature for mixture weight softmax
n_samplesint100Number of samples for MDN prediction
learning_ratefloat1e-3Learning rate for Adam optimizer
max_epochsint100Maximum training epochs
early_stopping_patienceint10Epochs to wait before early stopping
batch_sizeint256Training batch size
model_dirstr“./microimpute_models”Directory for caching trained models
force_retrainboolFalseIf True, skip cache and always retrain
seedint42Random seed for reproducibility
log_levelstr“WARNING”Logging level (DEBUG, INFO, WARNING, ERROR)

fit() method

def fit(
    X_train: pd.DataFrame,
    predictors: List[str],
    imputed_variables: List[str],
    weight_col: Optional[str] = None,
    tune_hyperparameters: bool = False,
    n_trials: int = 10,
    cv_folds: int = 3
) -> Union[MDNResults, Tuple[MDNResults, Dict]]
ParameterTypeDefaultDescription
X_trainpd.DataFrame-Training data with predictors and target variables
predictorsList[str]-Column names to use as predictors
imputed_variablesList[str]-Column names of variables to impute
weight_colstrNoneColumn name for sampling weights
tune_hyperparametersboolFalseEnable Optuna-based hyperparameter tuning
n_trialsint10Number of Optuna trials for tuning
cv_foldsint3Number of cross-validation folds for tuning

It returns a MDNResults object (or tuple with best hyperparameters if tuning enabled).

MDNResults.predict() method

def predict(
    X_test: pd.DataFrame,
    quantiles: Optional[List[float]] = None
) -> Dict[float, pd.DataFrame]
ParameterTypeDefaultDescription
X_testpd.DataFrame-Data to impute (with predictors)
quantilesList[float]QUANTILESQuantiles at which to return predictions

It returns a dictionary mapping quantiles to DataFrames of imputed values.

Setup and data preparation

Loading...
Training set size: 353 records
Testing set size: 89 records

Simulating missing data

For this example, we simulate missing data in our test set by removing the values we want to impute.

Loading...

Training and using the MDN imputer

Now we train the MDN imputer and use it to impute the missing values in our test set. The MDN model automatically caches trained models based on a hash of the input data, speeding up repeated analyses.

Loading...

Evaluating the imputation results

Now let’s compare the imputed values with the actual values to evaluate the performance of our imputer.

Loading...

This scatter plot compares actual observed values with those imputed by the MDN model. Each point represents a data record, with the x-axis showing the true value and the y-axis showing the model’s prediction closest to the true value across all quantiles. The red dashed line represents the ideal 1:1 relationship. Points clustering around this line indicate that the MDN model effectively captures the underlying structure of the data.

Examining quantile predictions

MDN provides predictions at different quantiles by sampling from the learned mixture distribution, allowing us to capture the entire conditional distribution of the missing values.

Loading...

Visualizing prediction intervals

By visualizing the prediction intervals we can better understand the uncertainty in our imputed values.

Loading...

This plot visualizes the prediction intervals produced by the MDN model. Each vertical bar represents an 80% (light gray) or 40% (dark gray) prediction interval. Red dots mark the model’s median predictions (Q50), while black dots show the actual observed values. In most cases, the true values fall within the prediction intervals, indicating that the MDN model appropriately captures uncertainty in its imputation.

Assessing the method’s performance

To verify our model doesn’t overfit and ensure robust results, we perform cross-validation.

[Parallel(n_jobs=-1)]: Using backend LokyBackend with 8 concurrent workers.
[Parallel(n_jobs=-1)]: Done   2 out of   5 | elapsed:    9.5s remaining:   14.3s
[Parallel(n_jobs=-1)]: Done   3 out of   5 | elapsed:    9.5s remaining:    6.4s
[Parallel(n_jobs=-1)]: Done   5 out of   5 | elapsed:    9.5s finished
Quantile loss results:
           0.05      0.10      0.15      0.20      0.25  ...      0.75      0.80      0.85      0.90      0.95
train  0.046876  0.074835  0.094329  0.108058  0.117289  ...  0.125522  0.116335  0.102234  0.081915  0.052980
test   0.048934  0.078406  0.099249  0.117245  0.130767  ...  0.133579  0.123578  0.109620  0.088337  0.058313

[2 rows x 19 columns]
Loading...

Hyperparameter tuning

MDN supports automatic hyperparameter tuning using Optuna. This tunes the number of Gaussian components and learning rate to optimize performance on your specific dataset. To tune hyperparameters set tune_hyperparameters=True, and access the best configuration found in the dictionary returned second when fitting the model.

fitted_tuned, best_parameters = mdn_tuned.fit(
    X_train,
    predictors,
    imputed_variables,
    tune_hyperparameters=True,
    n_trials=10,
    cv_folds=3,
)

Note this will require significant time due to multiple networks being trained in identifying the best hyperparameters.

Categorical variable imputation

MDN automatically handles categorical variables through neural classification. Let’s evaluate its performance on categorical imputation tasks.

Categorical variable distribution:
risk_level
medium    150
low       148
high      144
Name: count, dtype: int64

Training set size: 353 records
Testing set size: 89 records
MDN model fitted for categorical variable imputation
Categorical imputation accuracy: 30.34%

Confusion matrix:
                Predicted: low  Predicted: medium  Predicted: high
Actual: low                  8                  5               17
Actual: medium              11                  8               10
Actual: high                 7                 12               11
[Parallel(n_jobs=-1)]: Using backend LokyBackend with 8 concurrent workers.
[Parallel(n_jobs=-1)]: Done   2 out of   5 | elapsed:    1.2s remaining:    1.9s
[Parallel(n_jobs=-1)]: Done   3 out of   5 | elapsed:    1.3s remaining:    0.9s
[Parallel(n_jobs=-1)]: Done   5 out of   5 | elapsed:    1.4s finished
Categorical imputation cross-validation results (log loss):
Mean train log loss: 0.9076
Mean test log loss: 1.0931
Loading...

Model caching

MDN automatically caches trained models based on a hash of the input data. When you fit the model on the same data again, it loads from cache rather than retraining, significantly speeding up repeated analyses. Use force_retrain=True to bypass the cache if needed.