Features module tutorial

This tutorial shows how to perform an exploratory analysis of the features data: to visualize features distribution in classes, plot the feature correlation matrix, check Mann-Whitney U-test p-values, plot univariate ROC (and calculate AUC) for each feature, perform volumetric analysis, and save all the scores. We will use PyRadiomics features as variables and binary survival label as an outcome.

Importing modules:

import os,sys
from pmtool.AnalysisBox import AnalysisBox
import pandas as pd

Required data

To run the tutorial, you will need the following files, available in the 'data/features' folder of the precision-medicine-toolbox repository:
- 'extended_clinical_df.xlsx' - this is the clinical data file provided with the Lung1 dataset (Aerts et al., 2014); we added binary variables of 1-, 1.5-, and 2-years survival, based on the presented data,
- 'extracted_features_full.xlsx' - this is a file with the PyRadiomic features values (can be extracted in the imaging module tutorial) for all the patients from Lung1 dataset.

Binary classes functionality

If you have 2 classes in your dataset, the following functionality is availabe for your analysis. We will divide Lung1 dataset by 1 year survival label, therefore having 2 classes.

Set up the parameters to get the data:

parameters = {
    'feature_path': "../data/features/extracted_features_full.xlsx", # path to csv/xls file with features
    'outcome_path': "../data/features/extended_clinical_df.xlsx", #path to csv/xls file with outcome
    'patient_column': 'Patient', # name of column with patient ID
    'patient_in_outcome_column': 'PatientID', # name of column with patient ID in clinical data file
    'outcome_column': '1yearsurvival' # name of outcome column
}

Initialise the feature set (you will see a short summary):

fs = AnalysisBox(**parameters)
Number of observations: 149
Class labels: ['0' '1']
Classes balance: [0.4228187919463087, 0.5771812080536913]

Print some attributes of the feature set - first 10 patient IDs and first 10 feature names:

print ('Patient IDs: ', fs._patient_name[:10])
print ('\nFeature names: ', fs._feature_column[:10])
Patient IDs:  ['LUNG1-001_20180209_CT_2_GTV-1_mask', 'LUNG1-002_000000_GTV-1_mask', 'LUNG1-002_20180526_CT_1_GTV-1_mask', 'LUNG1-003_000000_GTV-1_mask', 'LUNG1-003_20180209_CT_1_GTV-1_mask', 'LUNG1-003_20180209_CT_1_GTV-2_mask', 'LUNG1-003_20180209_CT_1_GTV-3_mask', 'LUNG1-004_000000_GTV-1_mask', 'LUNG1-006_000000_GTV-1_mask', 'LUNG1-008_000000_GTV-1_mask']

Feature names:  ['original_shape_Elongation', 'original_shape_Flatness', 'original_shape_LeastAxisLength', 'original_shape_MajorAxisLength', 'original_shape_Maximum2DDiameterColumn', 'original_shape_Maximum2DDiameterRow', 'original_shape_Maximum2DDiameterSlice', 'original_shape_Maximum3DDiameter', 'original_shape_MeshVolume', 'original_shape_MinorAxisLength']

Exclude patients with the unknown outcomes, if they are represented in the dataset (feature set parameters are re-initialised and short summary is printed again):

fs.handle_nan(axis=0)
Number of observations: 149
Class labels: ['0' '1']
Classes balance: [0.4228187919463087, 0.5771812080536913]

Print the head of the composed dataframe, containing both the variables and the outcome:

fs._feature_outcome_dataframe.head(5)
original_shape_Elongation original_shape_Flatness original_shape_LeastAxisLength original_shape_MajorAxisLength original_shape_Maximum2DDiameterColumn original_shape_Maximum2DDiameterRow original_shape_Maximum2DDiameterSlice original_shape_Maximum3DDiameter original_shape_MeshVolume original_shape_MinorAxisLength ... wavelet-LLL_gldm_HighGrayLevelEmphasis wavelet-LLL_gldm_LargeDependenceEmphasis wavelet-LLL_gldm_LargeDependenceHighGrayLevelEmphasis wavelet-LLL_gldm_LargeDependenceLowGrayLevelEmphasis wavelet-LLL_gldm_LowGrayLevelEmphasis wavelet-LLL_gldm_SmallDependenceEmphasis wavelet-LLL_gldm_SmallDependenceHighGrayLevelEmphasis wavelet-LLL_gldm_SmallDependenceLowGrayLevelEmphasis ROI 1yearsurvival
Patient
LUNG1-001_20180209_CT_2_GTV-1_mask 0.732658 0.548834 46.151744 84.090588 95.336247 83.186537 95.425364 96.104110 155379.500000 61.609664 ... 14462.536758 33.609142 548084.071741 0.002759 0.000139 0.267761 2959.571494 0.000058 GTV-1_mask 1
LUNG1-002_000000_GTV-1_mask 0.878035 0.755488 70.110114 92.801132 116.931604 101.833197 104.316825 125.674182 358446.791667 81.482668 ... 13208.125493 55.600107 832176.248523 0.004497 0.000162 0.188931 1733.836805 0.000053 GTV-1_mask 0
LUNG1-002_20180526_CT_1_GTV-1_mask 0.878035 0.755488 70.110114 92.801132 116.931604 101.833197 104.316825 125.674182 358446.791667 81.482668 ... 13208.125493 55.600107 832176.248523 0.004497 0.000162 0.188931 1733.836805 0.000053 GTV-1_mask 0
LUNG1-003_000000_GTV-1_mask 0.544631 0.356597 25.559022 71.674815 56.639209 83.528438 62.265560 84.011904 34987.000000 39.036358 ... 9142.646956 17.909008 209143.444093 0.002673 0.000367 0.402930 2838.784544 0.000191 GTV-1_mask 0
LUNG1-003_20180209_CT_1_GTV-1_mask 0.544631 0.356597 25.559022 71.674815 56.639209 83.528438 62.265560 84.011904 34987.000000 39.036358 ... 9142.646956 17.909008 209143.444093 0.002673 0.000367 0.402930 2838.784544 0.000191 GTV-1_mask 0

5 rows × 1220 columns

Visualize feature values distribution in classes for the first 12 features (will pop up in an interactive .html report):

fs.plot_distribution(fs._feature_column[:8])

Shapiro-Wilcoxon test to get a list of normally distributed features:

features_norm_distr = fs.normality_check()
print (len(features_norm_distr))
print (features_norm_distr)
59
['original_shape_Sphericity', 'original_glcm_Idm', 'original_glcm_Id', 'original_glrlm_RunLengthNonUniformityNormalized', 'original_glszm_HighGrayLevelZoneEmphasis', 'original_glszm_SmallAreaHighGrayLevelEmphasis', 'original_gldm_SmallDependenceHighGrayLevelEmphasis', 'log-sigma-1-0-mm-3D_glcm_DifferenceEntropy', 'log-sigma-2-0-mm-3D_firstorder_MeanAbsoluteDeviation', 'log-sigma-3-0-mm-3D_firstorder_InterquartileRange', 'log-sigma-3-0-mm-3D_firstorder_RobustMeanAbsoluteDeviation', 'log-sigma-3-0-mm-3D_glcm_Id', 'log-sigma-4-0-mm-3D_firstorder_InterquartileRange', 'log-sigma-4-0-mm-3D_glcm_Idm', 'log-sigma-4-0-mm-3D_glcm_Id', 'log-sigma-4-0-mm-3D_glszm_SizeZoneNonUniformityNormalized', 'log-sigma-5-0-mm-3D_firstorder_Skewness', 'log-sigma-5-0-mm-3D_glcm_Imc1', 'log-sigma-5-0-mm-3D_glcm_Idm', 'log-sigma-5-0-mm-3D_glcm_Id', 'wavelet-LLH_firstorder_Entropy', 'wavelet-LLH_glcm_SumEntropy', 'wavelet-LHL_glcm_DifferenceAverage', 'wavelet-LHH_firstorder_10Percentile', 'wavelet-LHH_firstorder_MeanAbsoluteDeviation', 'wavelet-LHH_glcm_Correlation', 'wavelet-LHH_glcm_DifferenceAverage', 'wavelet-LHH_glcm_Idm', 'wavelet-LHH_glcm_Id', 'wavelet-LHH_glrlm_RunVariance', 'wavelet-HLL_glcm_DifferenceEntropy', 'wavelet-HLL_glcm_Imc1', 'wavelet-HLL_glcm_Idm', 'wavelet-HLL_glcm_Id', 'wavelet-HLL_glrlm_RunLengthNonUniformityNormalized', 'wavelet-HLL_glrlm_RunPercentage', 'wavelet-HLH_glcm_Correlation', 'wavelet-HLH_glcm_Idm', 'wavelet-HLH_glcm_Id', 'wavelet-HLH_glcm_InverseVariance', 'wavelet-HHL_firstorder_Entropy', 'wavelet-HHL_firstorder_MeanAbsoluteDeviation', 'wavelet-HHL_firstorder_Uniformity', 'wavelet-HHL_glcm_Correlation', 'wavelet-HHL_glcm_DifferenceEntropy', 'wavelet-HHL_glcm_JointEntropy', 'wavelet-HHL_glcm_Imc2', 'wavelet-HHL_glcm_MaximumProbability', 'wavelet-HHL_glcm_SumEntropy', 'wavelet-HHL_glrlm_ShortRunEmphasis', 'wavelet-HHH_firstorder_10Percentile', 'wavelet-HHH_firstorder_90Percentile', 'wavelet-HHH_firstorder_InterquartileRange', 'wavelet-HHH_firstorder_MeanAbsoluteDeviation', 'wavelet-HHH_firstorder_RobustMeanAbsoluteDeviation', 'wavelet-HHH_glcm_Imc2', 'wavelet-HHH_glcm_MaximumProbability', 'wavelet-LLL_glszm_HighGrayLevelZoneEmphasis', 'wavelet-LLL_gldm_SmallDependenceHighGrayLevelEmphasis']

Visualize mutual feature correlation coefficient (Spearman's) matrix for the first 12 features (in .html report):

fs.plot_correlation_matrix(fs._feature_column[:8])

Visualize Mann-Whitney (Bonferroni corrected) p-values for binary classes test (in .html report):

fs.plot_MW_p(fs._feature_column[:8])

Vizualize univariate ROC curves:

fs.plot_univariate_roc(fs._feature_column[:8], auc_threshold=0.70)

Calculate the basic statistics for each feature (save the values in data/features/extracted_features_full_basic_stats.xlsx): number of NaN, mean, std, min, max; if applicable: MW-p, univariate ROC AUC, volume correlation:

fs.calculate_basic_stats(volume_feature='original_shape_VoxelVolume')

Check the excel table:

print('Basic statistics for each feature')
pd.read_excel('../data/features/extracted_features_full_basic_stats.xlsx')
Basic statistics for each feature
Unnamed: 0 NaN Mean Std Min Max p_MW_corrected univar_auc volume_corr p_shapiro_test
0 original_shape_Elongation 0 0.720328 0.161721 0.062127 0.974104 1.000000 0.517996 0.037515 1.785652e-06
1 original_shape_Flatness 0 0.559677 0.154895 0.047315 0.856767 1.000000 0.516611 0.099032 2.595303e-03
2 original_shape_LeastAxisLength 0 32.055804 15.983620 6.643777 85.495660 0.117975 0.686877 0.973238 3.124696e-04
3 original_shape_MajorAxisLength 0 61.595666 35.336125 13.611433 240.822486 0.018917 0.705795 0.842114 1.637431e-09
4 original_shape_Maximum2DDiameterColumn 0 63.064956 33.057474 15.620499 157.632484 0.161563 0.681525 0.950909 3.372626e-05
... ... ... ... ... ... ... ... ... ... ...
1213 wavelet-LLL_gldm_LargeDependenceLowGrayLevelEm... 0 1.646810 8.820031 0.001036 79.946280 1.000000 0.524640 0.070593 3.766094e-25
1214 wavelet-LLL_gldm_LowGrayLevelEmphasis 0 0.007969 0.039767 0.000069 0.369746 0.471623 0.515319 -0.805811 5.147920e-25
1215 wavelet-LLL_gldm_SmallDependenceEmphasis 0 0.313546 0.177561 0.009452 0.755977 1.000000 0.654393 -0.634677 1.122224e-04
1216 wavelet-LLL_gldm_SmallDependenceHighGrayLevelE... 0 2431.019809 1077.764252 0.093515 5302.913608 1.000000 0.619970 -0.274001 3.676662e-01
1217 wavelet-LLL_gldm_SmallDependenceLowGrayLevelEm... 0 0.000440 0.000872 0.000013 0.007314 0.077728 0.654854 -0.872534 1.102855e-20

1218 rows × 10 columns

Volume analysis will show you if your features have a high correlation to volume and if volume itself is a predictive feature in separation of 2 classes. You need to have a volume feature in your dataset and send it as a function parameter (in our case it is 'original_shape_VoxelVolume').

fs.volume_analysis(volume_feature='original_shape_VoxelVolume')

Multi-class

If you have more than 2 classes in your dataset, the following functionality is availabe for your analysis. We will use a disease stage as a class label.

Set up the parameters to get the data:

parameters = {
    'feature_path': "../data/features/extracted_features_full.xlsx", # path to csv/xls file with features
    'outcome_path': "../data/features/extended_clinical_df.xlsx", # path to csv/xls file with outcome
    'patient_column': 'Patient', # name of column with patient ID
    'patient_in_outcome_column': 'PatientID', # name of column with patient ID in clinical data file
    'outcome_column': 'Overall.Stage' # name of outcome column
}

Initialise the feature set:

fs = AnalysisBox(**parameters)
Number of observations: 149
Class labels: ['I' 'II' 'IIIa' 'IIIb' 'nan']
Classes balance: [0.24161073825503357, 0.09395973154362416, 0.2348993288590604, 0.4228187919463087, 0.006711409395973154]

Check the patients with the absent outcomes (because there is a NaN value among class labels in the data summary):

fs._feature_outcome_dataframe[fs._feature_outcome_dataframe['Overall.Stage'].isnull()]
original_shape_Elongation original_shape_Flatness original_shape_LeastAxisLength original_shape_MajorAxisLength original_shape_Maximum2DDiameterColumn original_shape_Maximum2DDiameterRow original_shape_Maximum2DDiameterSlice original_shape_Maximum3DDiameter original_shape_MeshVolume original_shape_MinorAxisLength ... wavelet-LLL_gldm_HighGrayLevelEmphasis wavelet-LLL_gldm_LargeDependenceEmphasis wavelet-LLL_gldm_LargeDependenceHighGrayLevelEmphasis wavelet-LLL_gldm_LargeDependenceLowGrayLevelEmphasis wavelet-LLL_gldm_LowGrayLevelEmphasis wavelet-LLL_gldm_SmallDependenceEmphasis wavelet-LLL_gldm_SmallDependenceHighGrayLevelEmphasis wavelet-LLL_gldm_SmallDependenceLowGrayLevelEmphasis ROI Overall.Stage
Patient
LUNG1-272_000000_GTV-1_mask 0.713259 0.487347 14.2573 29.254915 25.495098 31.400637 35.608988 37.868192 5713.416667 20.86634 ... 11955.686684 27.442472 349332.116971 0.002377 0.00028 0.233726 2319.671592 0.000215 GTV-1_mask NaN

1 rows × 1220 columns

Exclude the patients with the unknown outcomes:

fs.handle_nan(axis=0)
Number of observations: 148
Class labels: ['I' 'II' 'IIIa' 'IIIb']
Classes balance: [0.24324324324324326, 0.0945945945945946, 0.23648648648648649, 0.42567567567567566]

Visualize feature values distribution in classes for the first 12 features (in .html report):

fs.plot_distribution(fs._feature_column[:8])

Visualize feature values distribution in classes for the first 12 features only for the selected classes (in .html report):

fs.plot_distribution(fs._feature_column[:8], ['I', 'IIIb'])

Visualize mutual feature correlation coefficient (Spearman's) matrix for the first 12 features (in .html report):

fs.plot_correlation_matrix(fs._feature_column[:12])

Even though we have more than 2 classes and cannot perform Mann-Whitney test for all the classes, we can perform it for the pairs of the classes (result in .html report):

fs.plot_MW_p(fs._feature_column[:100], ['I', 'IIIb'])

The same situation is with univariate analysis - we can perform it for the selected pairs of the classes:

fs.plot_univariate_roc(fs._feature_column[:100], ['I', 'IIIb'])

Calculate the basic statistics for each feature (save the values in data/features/features_basic_stats.xlsx); for multi-class analysis the following are available: number of NaN, mean, std, min, max

fs.calculate_basic_stats(volume_feature='original_shape_VoxelVolume')

Check the table:

print('Basic statistics for each feature')
pd.read_excel('../data/features/extracted_features_full_basic_stats.xlsx')
Basic statistics for each feature
Unnamed: 0 NaN Mean Std Min Max volume_corr
0 original_shape_Elongation 0 0.720328 0.161721 0.062127 0.974104 0.037515
1 original_shape_Flatness 0 0.559677 0.154895 0.047315 0.856767 0.099032
2 original_shape_LeastAxisLength 0 32.055804 15.983620 6.643777 85.495660 0.973238
3 original_shape_MajorAxisLength 0 61.595666 35.336125 13.611433 240.822486 0.842114
4 original_shape_Maximum2DDiameterColumn 0 63.064956 33.057474 15.620499 157.632484 0.950909
... ... ... ... ... ... ... ...
1213 wavelet-LLL_gldm_LargeDependenceLowGrayLevelEm... 0 1.646810 8.820031 0.001036 79.946280 0.070593
1214 wavelet-LLL_gldm_LowGrayLevelEmphasis 0 0.007969 0.039767 0.000069 0.369746 -0.805811
1215 wavelet-LLL_gldm_SmallDependenceEmphasis 0 0.313546 0.177561 0.009452 0.755977 -0.634677
1216 wavelet-LLL_gldm_SmallDependenceHighGrayLevelE... 0 2431.019809 1077.764252 0.093515 5302.913608 -0.274001
1217 wavelet-LLL_gldm_SmallDependenceLowGrayLevelEm... 0 0.000440 0.000872 0.000013 0.007314 -0.872534

1218 rows × 7 columns

Volume analysis (result in .html report):

fs.volume_analysis(volume_feature='original_shape_VoxelVolume')