web-dev-qa-db-fra.com

Facteur d'inflation de la variance en python

J'essaie de calculer le facteur d'inflation de la variance (VIF) pour chaque colonne d'un jeu de données simple en python: 

a b c d
1 2 4 4
1 2 6 3
2 3 7 4
3 2 8 5
4 1 9 4

Je l'ai déjà fait dans R en utilisant la fonction vif de la bibliothèque usdm qui donne les résultats suivants:

a <- c(1, 1, 2, 3, 4)
b <- c(2, 2, 3, 2, 1)
c <- c(4, 6, 7, 8, 9)
d <- c(4, 3, 4, 5, 4)

df <- data.frame(a, b, c, d)
vif_df <- vif(df)
print(vif_df)

Variables   VIF
   a        22.95
   b        3.00
   c        12.95
   d        3.00

Cependant, lorsque je fais la même chose en python en utilisant la fonction statsmodel vif , mes résultats sont les suivants:

a = [1, 1, 2, 3, 4]
b = [2, 2, 3, 2, 1]
c = [4, 6, 7, 8, 9]
d = [4, 3, 4, 5, 4]

ck = np.column_stack([a, b, c, d])

vif = [variance_inflation_factor(ck, i) for i in range(ck.shape[1])]
print(vif)

Variables   VIF
   a        47.136986301369774
   b        28.931506849315081
   c        80.31506849315096
   d        40.438356164383549

Les résultats sont très différents, même si les entrées sont les mêmes. En général, les résultats de la fonction VIF de statsmodel semblent erronés, mais je ne suis pas sûr que cela soit dû à la façon dont je l'appelle ou s'il s'agit d'un problème avec la fonction elle-même. 

J'espérais que quelqu'un pourrait m'aider à déterminer si j'appelais de manière incorrecte la fonction statsmodel ou à expliquer les divergences dans les résultats. S'il y a un problème avec la fonction, existe-t-il des alternatives VIF en python?

7
Nizag

Je crois que la raison en est due à une différence dans les MLS de Python. OLS, qui est utilisé dans le calcul du facteur d’inflation de la variance python, n’ajoute pas d’interception par défaut. Vous voulez vraiment une interception ici cependant.

Ce que vous voudriez faire est d’ajouter une colonne de plus à votre matrice, ck, remplie de celles représentant une constante. Ce sera le terme d'interception de l'équation. Une fois que cela est fait, vos valeurs doivent correspondre correctement.

Édité: zéros remplacés par des zéros

8
Drverzal

Comme mentionné par d'autres et dans ce message de Josef Perktold, l'auteur de la fonction, variance_inflation_factor s'attend à la présence d'une constante dans la matrice de variables explicatives. On peut utiliser add_constant de statsmodels pour ajouter la constante requise à la structure de données avant de transmettre ses valeurs à la fonction.

from statsmodels.stats.outliers_influence import variance_inflation_factor
from statsmodels.tools.tools import add_constant

df = pd.DataFrame(
    {'a': [1, 1, 2, 3, 4],
     'b': [2, 2, 3, 2, 1],
     'c': [4, 6, 7, 8, 9],
     'd': [4, 3, 4, 5, 4]}
)

X = add_constant(df)
>>> pd.Series([variance_inflation_factor(X.values, i) 
               for i in range(X.shape[1])], 
              index=X.columns)
const    136.875
a         22.950
b          3.000
c         12.950
d          3.000
dtype: float64

Je pense que vous pouvez également ajouter la constante à la colonne la plus à droite du cadre de données en utilisant assign:

X = df.assign(const=1)
>>> pd.Series([variance_inflation_factor(X.values, i) 
               for i in range(X.shape[1])], 
              index=X.columns)
a         22.950
b          3.000
c         12.950
d          3.000
const    136.875
dtype: float64

Le code source lui-même est plutôt concis:

def variance_inflation_factor(exog, exog_idx):
    """
    exog : ndarray, (nobs, k_vars)
        design matrix with all explanatory variables, as for example used in
        regression
    exog_idx : int
        index of the exogenous variable in the columns of exog
    """
    k_vars = exog.shape[1]
    x_i = exog[:, exog_idx]
    mask = np.arange(k_vars) != exog_idx
    x_noti = exog[:, mask]
    r_squared_i = OLS(x_i, x_noti).fit().rsquared
    vif = 1. / (1. - r_squared_i)
    return vif

Il est également assez simple de modifier le code pour renvoyer tous les fichiers VIF sous forme de série:

from statsmodels.regression.linear_model import OLS
from statsmodels.tools.tools import add_constant

def variance_inflation_factors(exog_df):
    '''
    Parameters
    ----------
    exog_df : dataframe, (nobs, k_vars)
        design matrix with all explanatory variables, as for example used in
        regression.

    Returns
    -------
    vif : Series
        variance inflation factors
    '''
    exog_df = add_constant(exog_df)
    vifs = pd.Series(
        [1 / (1. - OLS(exog_df[col].values, 
                       exog_df.loc[:, exog_df.columns != col].values).fit().rsquared) 
         for col in exog_df],
        index=exog_df.columns,
        name='VIF'
    )
    return vifs

>>> variance_inflation_factors(df)
const    136.875
a         22.950
b          3.000
c         12.950
Name: VIF, dtype: float64
5
Alexander

Pour les futurs venus sur ce sujet (comme moi):

import numpy as np
import scipy as sp

a = [1, 1, 2, 3, 4]
b = [2, 2, 3, 2, 1]
c = [4, 6, 7, 8, 9]
d = [4, 3, 4, 5, 4]

ck = np.column_stack([a, b, c, d])
cc = sp.corrcoef(ck, rowvar=False)
VIF = np.linalg.inv(cc)
VIF.diagonal()

Ce code donne

array([22.95,  3.  , 12.95,  3.  ])

[MODIFIER]

En réponse à un commentaire, j'ai essayé d'utiliser DataFrame autant que possible (numpy est requis pour inverser une matrice). 

import pandas as pd
import numpy as np

a = [1, 1, 2, 3, 4]
b = [2, 2, 3, 2, 1]
c = [4, 6, 7, 8, 9]
d = [4, 3, 4, 5, 4]

df = pd.DataFrame({'a':a,'b':b,'c':c,'d':d})
df_cor = df.corr()
pd.DataFrame(np.linalg.inv(df.corr().values), index = df_cor.index, columns=df_cor.columns)

Le code donne

       a            b           c           d
a   22.950000   6.453681    -16.301917  -6.453681
b   6.453681    3.000000    -4.080441   -2.000000
c   -16.301917  -4.080441   12.950000   4.080441
d   -6.453681   -2.000000   4.080441    3.000000

Les éléments en diagonale donnent VIF.

3
T_T

Exemple pour Boston Data :

VIFest calculé par régression auxiliaire, il ne dépend donc pas de l'ajustement réel. 

Voir ci-dessous:

from patsy import dmatrices
from statsmodels.stats.outliers_influence import variance_inflation_factor
import statsmodels.api as sm

# Break into left and right hand side; y and X
y, X = dmatrices(formula="medv ~ crim + zn + nox + ptratio + black + rm ", data=boston, return_type="dataframe")

# For each Xi, calculate VIF
vif = [variance_inflation_factor(X.values, i) for i in range(X.shape[1])]

# Fit X to y
result = sm.OLS(y, X).fit()
1
Saqib Mujtaba

J'ai écrit cette fonction à partir d'autres articles que j'ai vus sur Stack et CrossValidated. Il affiche les fonctionnalités dépassant le seuil et renvoie une nouvelle image de données avec les fonctionnalités supprimées.

from statsmodels.stats.outliers_influence import variance_inflation_factor 
from statsmodels.tools.tools import add_constant

def calculate_vif_(df, thresh=5):
    '''
    Calculates VIF each feature in a pandas dataframe
    A constant must be added to variance_inflation_factor or the results will be incorrect

    :param X: the pandas dataframe
    :param thresh: the max VIF value before the feature is removed from the dataframe
    :return: dataframe with features removed
    '''
    const = add_constant(df)
    cols = const.columns
    variables = np.arange(const.shape[1])
    vif_df = pd.Series([variance_inflation_factor(const.values, i) 
               for i in range(const.shape[1])], 
              index=const.columns).to_frame()

    vif_df = vif_df.sort_values(by=0, ascending=False).rename(columns={0: 'VIF'})
    vif_df = vif_df.drop('const')
    vif_df = vif_df[vif_df['VIF'] > thresh]

    print 'Features above VIF threshold:\n'
    print vif_df[vif_df['VIF'] > thresh]

    col_to_drop = list(vif_df.index)

    for i in col_to_drop:
        print 'Dropping: {}'.format(i)
        df = df.drop(columns=i)

    return df
0
Chef1075

Au cas où vous ne voudriez pas vous occuper de variance_inflation_factor et add_constant et que vous souhaitiez simplement utiliser une formule Veuillez considérer la fonction suivante.

import pandas as pd
import seaborn as sns
import numpy as np
import statsmodels.formula.api as smf
# define function
def get_vif(exogs, data):
    '''Return VIF (variance inflation factor) DataFrame

    Args:
    exogs (list): list of exogenous/independent variables
    data (DataFrame): the df storing all variables

    Returns:
    VIF and Tolerance DataFrame for each exogenous variable

    Notes:
    Assume we have a list of exogenous variable [X1, X2, X3, X4].
    To calculate the VIF and Tolerance for each variable, we regress
    each of them against other exogenous variables. For instance, the
    regression model for X3 is defined as:
                        X3 ~ X1 + X2 + X4
    And then we extract the R-squared from the model to calculate:
                    VIF = 1 / (1 - R-squared)
                    Tolerance = 1 - R-squared
    The cutoff to detect multicollinearity:
                    VIF > 10 or Tolerance < 0.2
    '''

    # initialize arrays
    vif_array = np.array([])
    tolerance_array = np.array([])
    # create formula for each exogenous variable
    for exog in exogs:
        not_exog = [i for i in exogs if i != exog]
        formula = f"{exog} ~ {' + '.join(not_exog)}"
        # extract r-squared from the fit
        r_squared = smf.ols(formula, data=data).fit().rsquared
        # calculate VIF
        vif = 1/(1-r_squared)
        vif_array = np.append(vif_array, vif).round(2)
        # calculate tolerance
        tolerance = 1-r_squared
        tolerance_array = np.append(tolerance_array, tolerance)
    # return VIF DataFrame
    df_vif = pd.DataFrame({'VIF': vif_array, 'Tolerance': tolerance_array},
                          index=exogs)
    return df_vif

[In]
df = sns.load_dataset('car_crashes')
exogs = ['alcohol', 'speeding', 'no_previous', 'not_distracted']
get_vif(exogs=exogs, data=df)

[Out]
                 VIF  Tolerance
alcohol         3.44   0.291030
speeding        1.88   0.530690
no_previous     3.11   0.321132
not_distracted  2.67   0.374749
0
steven