From 8379fca90feb176821125cf2fd9e7e6345342dbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cain=C3=A3=20Silva?= Date: Thu, 25 Apr 2024 00:58:26 -0300 Subject: [PATCH 1/7] update EqualFrequencyDiscretiser user guide text and images --- .../EqualFrequencyDiscretiser.rst | 328 ++++++++++++++---- 1 file changed, 262 insertions(+), 66 deletions(-) diff --git a/docs/user_guide/discretisation/EqualFrequencyDiscretiser.rst b/docs/user_guide/discretisation/EqualFrequencyDiscretiser.rst index be23124eb..81f8bfa59 100644 --- a/docs/user_guide/discretisation/EqualFrequencyDiscretiser.rst +++ b/docs/user_guide/discretisation/EqualFrequencyDiscretiser.rst @@ -5,122 +5,318 @@ EqualFrequencyDiscretiser ========================= -The :class:`EqualFrequencyDiscretiser()` sorts continuous numerical variables into -contiguous equal frequency intervals, that is, intervals that contain approximately the -same proportion of observations. The limits of the intervals are calculated according -to percentiles or quantiles utilising `pandas.qcut()`. You decide the number of -intervals. +The :class:`EqualFrequencyDiscretiser()` is a discretization method that applies the pandas.qcut() function to divide continuous numerical variables into equal-frequency bins. These bins contain roughly the same number of observations, with boundaries set at specific quantile values determined by the desired number of bins (q parameter). This method ensures a uniform distribution of data points across the range of values, enhancing the handling of skewed data and outliers. -**A note on number of intervals** +Discretization is a common data preprocessing technique used in data science. It's also known as binning data (or simply `binning`). -Common values are 5 and 10. Note that if the variable is highly skewed or not continuous -smaller intervals maybe required. Otherwise, the transformer will introduce np.nan. +Advantages and Limitations +-------------------------- -The :class:`EqualFrequencyDiscretiser()` works only with numerical variables. A list of -variables can be indicated, or the discretiser will automatically select all numerical -variables in the train set. +Advantages +~~~~~~~~~~ -**Example** +:class:`EqualFrequencyDiscretiser()` has the same advantages as the classic equal width discretizer: -Let's look at an example using the House Prices Dataset (more details about the -dataset :ref:`here `). +- **Algorithm Efficiency:** Enhances the performance of data mining and machine learning algorithms by providing a simplified representation of the dataset. +- **Outlier Management:** Efficiently mitigates the effect of outliers by grouping them into the extreme bins, thus preserving the integrity of the main data distribution. +- **Data Smoothing:** Helps smooth the data, reduces noise, and improves the model's ability to generalize. -Let's load the house prices dataset and separate it into train and test sets: +Plus, it improves the data distribution, **optimizing the spread of values**. This is particularly beneficial in datasets with skewed distributions (see the Python example code). + +Limitations +~~~~~~~~~~~ + +On the other hand, :class:`EqualFrequencyDiscretiser()` can lead to a loss of information by aggregating data into broader categories. This is particularly concerning if the data in the same bin has predictive information about the target. + +Let's consider a binary classifier task using a decision tree model. A bin with a high proportion of both categories would potentially impact the model's performance in this scenario. + +Notes +----- + +`EqualFrequencyDiscretiser` expects a `pandas.DataFrame` and works only with numerical variables. The user can specify the variables to be discretized. Otherwise, `EqualFrequencyDiscretiser` will automatically infer the data types to compute the interval limits for all numeric variables. + +**Optimal number of intervals:** With `EqualFrequencyDiscretiser`, the user defines the number of bins. Smaller intervals may be required if the variable is highly skewed or not continuous. Otherwise, the transformer will introduce `numpy.nan`. + +**Integration with scikit-learn:** `EqualFrequencyDiscretiser` and all other feature-engine transformers seamlessly integrate with scikit-learn [pipelines](https://scikit-learn.org/stable/modules/generated/sklearn.pipeline.Pipeline.html) and [column transformers](https://scikit-learn.org/stable/modules/generated/sklearn.compose.ColumnTransformer.html). + +Python code example +------------------- + +Load dataset +~~~~~~~~~~~~ + +In this example, we'll use the House Prices' Dataset (for more details, please check [this link](https://www.openml.org/search?type=data&sort=version&status=any&order=asc&exact_name=house_prices)). + +First, let's load the dataset and split it into train and test sets: .. code:: python - import numpy as np - import pandas as pd import matplotlib.pyplot as plt + from sklearn.datasets import fetch_openml from sklearn.model_selection import train_test_split from feature_engine.discretisation import EqualFrequencyDiscretiser # Load dataset - data = data = pd.read_csv('houseprice.csv') + X, y = fetch_openml(name='house_prices', version=1, return_X_y=True, as_frame=True) + X.set_index('Id', inplace=True) # Separate into train and test sets - X_train, X_test, y_train, y_test = train_test_split( - data.drop(['Id', 'SalePrice'], axis=1), - data['SalePrice'], test_size=0.3, random_state=0) + X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42) + -Now we want to discretise the 2 variables indicated below into 10 intervals of equal -number of observations: +Equal-frequency Discretisation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In this example, let's discretize two variables (LotArea and GrLivArea) into 10 intervals of approximately equal number of observations. .. code:: python + # List the target numeric variables to be transformed + TARGET_NUMERIC_FEATURES= ['LotArea','GrLivArea'] - # set up the discretisation transformer - disc = EqualFrequencyDiscretiser(q=10, variables=['LotArea', 'GrLivArea']) + # Set up the discretisation transformer + disc = EqualFrequencyDiscretiser(q=10, variables=TARGET_NUMERIC_FEATURES) - # fit the transformer + # Fit the transformer disc.fit(X_train) -With `fit()` the transformer learns the boundaries of each interval. Then, we can go -ahead and sort the values into the intervals: - -.. code:: python - # transform the data - train_t= disc.transform(X_train) - test_t= disc.transform(X_test) +Note that if we do not specify the variables (default=`None`), `EqualFrequencyDiscretiser` will automatically infer the data types to compute the interval limits for all numeric variables. -The `binner_dict_` stores the interval limits identified for each variable. +With the `.fit()` method, the discretiser learns the bin boundaries and saves them into a dictionary so we can use them to transform unseen data: .. code:: python + # Learnt limits for each variable disc.binner_dict_ + .. code:: python {'LotArea': [-inf, - 5007.1, - 7164.6, - 8165.700000000001, - 8882.0, - 9536.0, - 10200.0, - 11046.300000000001, - 12166.400000000001, - 14373.9, + 5000.0, + 7105.6, + 8099.200000000003, + 8874.0, + 9600.0, + 10318.400000000001, + 11173.5, + 12208.2, + 14570.699999999999, inf], 'GrLivArea': [-inf, - 912.0, - 1069.6000000000001, - 1211.3000000000002, - 1344.0, - 1479.0, - 1603.2000000000003, - 1716.0, + 918.5, + 1080.4, + 1218.0, + 1348.4, + 1476.5, + 1601.6000000000001, + 1717.6999999999998, 1893.0000000000005, 2166.3999999999996, inf]} -With equal frequency discretisation, each bin contains approximately the same number of observations. +Note that the lower and upper boundariers are set to -inf and inf, respectively. This behavior ensures the transformer works even for unseen limits (lower than the minimum or greater than the maximum trained value). + +Also, this transformer will not work in the presence of missing values. Therefore, we should either remove or impute missing values before fitting the transformer. .. code:: python - train_t.groupby('GrLivArea')['GrLivArea'].count().plot.bar() - plt.ylabel('Number of houses') + # Transform the data + train_t = disc.transform(X_train) + test_t = disc.transform(X_test) -We can see below that the intervals contain approximately the same number of -observations. -.. image:: ../../images/equalfrequencydiscretisation.png +Let's visualize the first rows of the raw data and the transformed data: -| +.. code:: python + + # Raw data + print(X_train[TARGET_NUMERIC_FEATURES].head()) + +.. code:: python + + LotArea GrLivArea + Id + 136 10400 1682 + 1453 3675 1072 + 763 8640 1547 + 933 11670 1905 + 436 10667 1661 + +.. code:: python + + # Transformed data + print(train_t[TARGET_NUMERIC_FEATURES].head()) + +.. code:: python + + LotArea GrLivArea + Id + 136 6 6 + 1453 0 1 + 763 3 5 + 933 7 8 + 436 6 6 + + +The transformed data now contains discrete values corresponding to the ordered computed buckets (0 being the first and q-1 the last). + +Now, let's visualize the plots for equal-width intervals (a common histogram) and the transformed data with equal-frequency discretiser: + +.. code:: python + + # Instantiate a figure with two axes + fig, axes = plt.subplots(ncols=2, figsize=(10,5)) + + # Plot raw distribution + X_train['GrLivArea'].plot.hist(bins=disc.q, ax=axes[0]) + axes[0].set_title('Raw data with equal width binning') + axes[0].set_xlabel('GrLivArea') + + # Plot transformed distribution + train_t['GrLivArea'].value_counts().sort_index().plot.bar(ax=axes[1]) + axes[1].set_title('Transformed data with equal frequency binning') + + plt.tight_layout(w_pad=2) + plt.show() + +Figure goes here... + +As we can see, the intervals contain approximately the same number of observations. + +Finally, since the default value for the `return_object` parameter is `False`, the transformer outputs integer variables: + +.. code:: python + + train_t[TARGET_NUMERIC_FEATURES].dtypes + + +.. code:: python + + LotArea int64 + GrLivArea int64 + dtype: object + + +Return object instead of integers +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Categorical encoders in feature-engine are designed to work by default with variables of type object. Therefore, to further encode the discretiser output with feature-engine, we can set `return_object=True` instead. This will return the transformed variables as object. + +Let's say we want to obtain monotonic relationships between the variable and the target. We can do that seamlessly by setting `return_object` to True. A tutorial of how to use this functionality is available [here](https://nbviewer.org/github/feature-engine/feature-engine-examples/blob/main/discretisation/EqualFrequencyDiscretiser_plus_WoEEncoder.ipynb). + +Additionally, if we want to output the intervals as object while specifying the boundaries, we can set `return_boundaries` to True: + +.. code:: python + + # Set up the discretisation transformer + disc = EqualFrequencyDiscretiser(q=10, variables=TARGET_NUMERIC_FEATURES, return_boundaries=True) + + # Fit the transformer + disc.fit(X_train) + + # Transform test set & visualize limit + test_t = disc.transform(X_test) + + # Visualize output (boundaries) + print(test_t[TARGET_NUMERIC_FEATURES].head()) + + +.. code:: python + + LotArea GrLivArea + Id + 893 (8099.2, 8874.0] (918.5, 1080.4] + 1106 (12208.2, 14570.7] (2166.4, inf] + 414 (8874.0, 9600.0] (918.5, 1080.4] + 523 (-inf, 5000.0] (1601.6, 1717.7] + 1037 (12208.2, 14570.7] (1601.6, 1717.7] + + +Binning skewed data +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code:: python + + import numpy as np + import pandas as pd + import matplotlib.pyplot as plt + from feature_engine.discretisation import EqualFrequencyDiscretiser + + +.. code:: python + + # Set seed for reproducibility + np.random.seed(42) + + # Generate a normally distributed data + normal_data = np.random.normal(loc=0, scale=1, size=1000) + + # Generate a right-skewed data using exponential distribution + skewed_data = np.random.exponential(scale=1, size=1000) + + # Create dataframe with simulated data + X = pd.DataFrame({'feature1': normal_data, 'feature2': skewed_data}) + + +.. code:: python + + # Instantiate discretiser + disc = EqualFrequencyDiscretiser(q=5) + + # Transform simulated data + X_transformed = disc.fit_transform(X) + + +.. code:: python + + # Plot raw and discretized data for normally distributed data + # binning method = equal width vs equal frequency + + fig, axes = plt.subplots(1, 2, figsize=(12, 4)) + + axes[0].hist(X.feature1, bins=disc.q) + axes[0].set(xlabel='feature1', ylabel='count', title='Raw data') + + X_transformed.feature1.value_counts().sort_index().plot.bar(ax=axes[1]) + axes[1].set_title('Transformed data') + + plt.suptitle('Normal distributed data', weight='bold', size='large', y=1.05) + + plt.show() + + +.. image:: ../../images/EqualFrequencyDiscretiser_gaussian.png + +.. code:: python + + # Plot raw and discretized data for skewed distributed data + # binning method = equal width vs equal frequency + + fig, axes = plt.subplots(1, 2, figsize=(12, 4)) + + axes[0].hist(X.feature2, bins=disc.q) + axes[0].set(xlabel='feature2', ylabel='count', title='Raw data') + + X_transformed.feature2.value_counts().sort_index().plot.bar(ax=axes[1]) + axes[1].set_title('Transformed data') + + plt.suptitle('Skewed distributed data', weight='bold', size='large', y=1.05) + + plt.show() + + +.. image:: ../../images/EqualFrequencyDiscretiser_skewed.png -**Discretisation plus encoding** +See Also +-------- -If we return the interval values as integers, the discretiser has the option to return -the transformed variable as integer or as object. Why would we want the transformed -variables as object? +- Further feature-engine discretiser / binning options [here](https://feature-engine.trainindata.com/en/latest/user_guide/discretisation/index.html) +- Scikit-learn [`KBinsDiscretizer`](https://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.KBinsDiscretizer.html#sklearn.preprocessing.KBinsDiscretizer) class +- [Pandas qcut](https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.qcut.html) -Categorical encoders in Feature-engine are designed to work with variables of type -object by default. Thus, if you wish to encode the returned bins further, say to try and -obtain monotonic relationships between the variable and the target, you can do so -seamlessly by setting `return_object` to True. You can find an example of how to use -this functionality `here `_. Additional resources -------------------- From 33935ae6946c2f0face9273cdfb6d7064e315988 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cain=C3=A3=20Silva?= Date: Thu, 25 Apr 2024 01:08:36 -0300 Subject: [PATCH 2/7] update EqualFrequencyDiscretiser user guide text and images --- docs/images/equalfrequencydiscretisation.png | Bin 6055 -> 28668 bytes .../equalfrequencydiscretisation_gaussian.png | Bin 0 -> 24631 bytes .../equalfrequencydiscretisation_skewed.png | Bin 0 -> 27858 bytes .../EqualFrequencyDiscretiser.rst | 8 +++++--- 4 files changed, 5 insertions(+), 3 deletions(-) create mode 100644 docs/images/equalfrequencydiscretisation_gaussian.png create mode 100644 docs/images/equalfrequencydiscretisation_skewed.png diff --git a/docs/images/equalfrequencydiscretisation.png b/docs/images/equalfrequencydiscretisation.png index febbf5ce8093a6c9031e97757a1ed913f7014e0a..9995229e6d5dfcf5439e109351cf7df966bc4c4e 100644 GIT binary patch literal 28668 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 diff --git a/docs/images/equalfrequencydiscretisation_gaussian.png b/docs/images/equalfrequencydiscretisation_gaussian.png new file mode 100644 index 0000000000000000000000000000000000000000..423801207f7651fde810b5a48e1373c31bc41d0c GIT binary patch literal 24631 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 KelF{r5}E)ECHbrX literal 0 HcmV?d00001 diff --git a/docs/user_guide/discretisation/EqualFrequencyDiscretiser.rst b/docs/user_guide/discretisation/EqualFrequencyDiscretiser.rst index 81f8bfa59..157fe292a 100644 --- a/docs/user_guide/discretisation/EqualFrequencyDiscretiser.rst +++ b/docs/user_guide/discretisation/EqualFrequencyDiscretiser.rst @@ -71,6 +71,7 @@ Equal-frequency Discretisation In this example, let's discretize two variables (LotArea and GrLivArea) into 10 intervals of approximately equal number of observations. .. code:: python + # List the target numeric variables to be transformed TARGET_NUMERIC_FEATURES= ['LotArea','GrLivArea'] @@ -182,7 +183,8 @@ Now, let's visualize the plots for equal-width intervals (a common histogram) an plt.tight_layout(w_pad=2) plt.show() -Figure goes here... + +.. image:: ../../images/equalfrequencydiscretisation_gaussian.png As we can see, the intervals contain approximately the same number of observations. @@ -288,7 +290,7 @@ Binning skewed data plt.show() -.. image:: ../../images/EqualFrequencyDiscretiser_gaussian.png +.. image:: ../../images/equalfrequencydiscretisation_gaussian.png .. code:: python @@ -308,7 +310,7 @@ Binning skewed data plt.show() -.. image:: ../../images/EqualFrequencyDiscretiser_skewed.png +.. image:: ../../images/equalfrequencydiscretisation_skewed.png See Also -------- From b149f29c59447584cd9cae4af9b97564866d8831 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cain=C3=A3=20Silva?= Date: Thu, 25 Apr 2024 02:25:27 -0300 Subject: [PATCH 3/7] update EqualWidthDiscretiser user guide text and image --- docs/images/equalwidthdiscretisation.png | Bin 6758 -> 14766 bytes .../discretisation/EqualWidthDiscretiser.rst | 229 +++++++++++++----- 2 files changed, 172 insertions(+), 57 deletions(-) diff --git a/docs/images/equalwidthdiscretisation.png b/docs/images/equalwidthdiscretisation.png index e30427d407e626f5db459b6f710439a56ab2b373..a90b01dbf844b9c006db30f72d64dfe74cdbc1cd 100644 GIT binary patch literal 14766 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 literal 6758 zcmb7J2UJtp)()XWqz0s@G$DWpQUrz$q5%m3r3z9sf{1|t5h5*=L{x&KGK4BsB~+y; zgeGuDVNg+{L8L?=>YxS;ph762{Fiz2-kbHm_m`KoZnARj$;ml;f8XBU-Z#VT=n;wS zyS76h5DAnM(gOk!pnw-_n+Q14T=7~L{1A#cg!0-3zLK_`0dGP?@lGdVAP_N`txJG8 zp0WlGS`Zw337%1B32~>RLm&~S33yx-0f!Aziw%j6!A3=@8S5MB8|$f^BM|VG1_u9} zpdS?-YEU9}w;Tf5`51+?_lhs(juV?tUP%7@Z3#vpDPDh)RoO0W02i|qv3AtcceoXO zH$bo1@CG!kaxkc`=A*i$7jD02K>7WL9_7`)x=S}5Le@H}wLOH0?g=<_3wbcZUgn6< zC7r_gn|8dpob*P9h1Jv1P&hnUBs_V>7g$QhnUBt;YLQ|@gJ}E+_y3G z>v}6AG;fY?o zt8=VTR{8g_K(bAGgH8x|PV#ZtOWf=9&yYcKJN2Z$@?c_%<#dZOr@#D3IGL#&%&G8K(!yEvBFSFvM3pHULy7 z>%rkom;KPvR1t)$4yzNI4viCE)Xi~6_d{EA56cTgc}_vtdss`Ya;K2UYxiEbyLgf> zQ)6k)$o!G+L(CznNOfV(amZ%fn;vQyWhSilbbS?F(I1?9r$y*AUmnv__&qJXCW^g4 z|BykUyvV-!2K3L}7h07gzdbM91pi1+45c{Erfx5aS)9)gFsx>6eBVnhxVq0^LfzXZptp1$jXP?uZJp8{o zAbX$qanpWjE~m)>mKdXXPqrD|EN5Rh2^&J(g;zbevbKkvVEVaO8+(%$GBex|WxPhA zaQ*IzKH@pDVn`eyfSoyAo4eG}DQs5Fg|DuR!V z0~usw8@sFPnS9Z9y_{wZZ}pB|o*Xq2rk}Tv3Ah5n6&u1pzGhuzM~rz>-v|ie9d(-9 zSgjELP|l?5kZKclrAJV^S?(ATi7S0hyYYXpa;z8z*C;UEWCcf;5*apOE-z)_O*QZS zJ9+;j&;H9~aDymgM4$#XmzGO?xpzu6XF&|5`)uY(Y4C(LwuNS;sr>dh4m^0rPi3$s zf%r`B6je*m%-Ju%){4P9lBkOwgPFre6qXOO`P7p^#NeNe9Fe_-ew}HJ$vxbu5I!6u zXBctmFi3CudK;HW8Uo(Yx9pvD9j$tab^=HN7AwA#P$RPzgi(zBA%i4-qNM-hC!(6_ z>V76+ve`6Q(%eW2l6eBy0PG~kl}*eyP5@@4I97@;a|K9;uD~%3=iHHh*KE!0KDX@u=|^GV(mVC-n+L?wxlyyue5+ufz-mxU8-H}+#{Y~_g{3+Z@W{9T${ z#n*KJMg8PvsyZ&Vrb*)FQuPHQt9A)oNOUxGKoY7@7nft$c(w*T)pHgyVKrt?PjyP} zd6cW66eEd0npOaY^^o!j(UBB!U)V$XjtIzXm~l0Q@@1l0^bm6txC!u`UnAI?%{#r2 zNJ0bZ)T8sjYoHuLABJYL#P_MIcMK!!*Iv?fVq_59w~u*62WOF4OPfsUQER@drd@_b=emx&WX;C-s{@| zH);ZxwJe*~i7wOON9_-ZiqhS(_{nUcZ~IscUq*ZUm@guP!L`?KKlRwXTeIujxhlPu zrlzi6z@=49P||b4^;yld=c#SG;u+^)sKPy*U8uW=x*P%+O#_)hsJPsv&w0_P)l0M? zkO~<)8%l3TvY5NWQ&Z>-vp_Zd5kLPjPuIhspua)PN9q>KAy05T9qF%TYd^~ zsgaP4@|ZE+YmE}$p}I%Hz&@7h%*r`9{s2BQq32=MS_xIZa=JKW@46I-n4L8l91Z`3 zS;3mKfs^~N4mw0T@1Qm$6ALiNvd=<&phuv0$^9hH*6*vJ!R4;FN>90$`#C!ps&H8d$wZgtrv%F82KWjoE=>i&S5#`U0%9c^CB$p!-K}Q!b`~N~<#S6=VkPhw@a{pxeP7nLZybt`Id0U;Nw`_q(SmyNkL*G>yaq}r*X)dFRpAK?(Ue3}5SSF%xDaD-mQTJN zfhrTM-`g2Z+G*omB^YkDwDu>CF<_O}<%YHT|D1rBQz%L`~bkDXwOkwyUt z|7dXcio3U1lNd7^pu=%y%haANlO58lNgdwbuW*)bU6<$8D$T6)f`7___sh>{-G2683~B+V%- zRn1nPgJTnckB2YQ^IPS;!F*MZx)^UUf1Hge#RWx=W`f|YMO`$Pj6Xd`odh@a&GaTJ z+1Rjxdl1A_k4bylh-}+orbp=*EbDar9~2QfA3Ng?Ej%6qtjKole&86{&@-D-AlEfO*plj0wNDgQAdiJ4?l4l?=b}fGgD^4XV3^ zjI3ETl{y`kgtfk|kT zfIO7XO2J!0!9*J=VFeCvisV5vAqK7MzrH|8Zc(9x#!7q^o8Roq44hhTuLz*p?g2HL zKZ4sMXXgoWft;oFtP2KeF+EHjEq!1Hj*>DnU&<#vHq$AR3J4+og!dkWnL`T#PyQhN zDB9cxp5n@Bu!2&?&4m%~b3>ONYMGr#f69|0or>~E68H9$n_3?!{Vo>RE?J9Rbz(XuLM%eRbu%SPLnZ7*1+T zMM+WdoCPQXxWnKb*pY8->5ea_2=|vIcnw2=>`6iQ`+<+{$XjRi@0K{>$X)-!r1Av%)F@KS6inq@2f;mvHVsKJ<>nnga zJyg&qcO3+?lbxxNvY?(GON<s;dqN=kr>u!2WqvL)SqB8LQU^UlRSX-x&Pu$qaLk_+Tpm&^3BQM^JiBV7^@Y(U<7q5wQ`v=+x^}!~ zvasMS#5j=$#($#j$VrHv;*1#D?<^62kI`ugPFz=%T?h@xsJYqe#c9>5tN?H*gI^ZJ z*+`6gzmWqHm%lnK`G_ZiniJ|qn?7F@qx-}v9S3DhO@oXOiK|Q2wQ5@^F!R4o?tSB8 z_nXlAPM@GKEu+_=C2zpUV7FQ)1vVa5k$(_oy3>~OO^s9GW!zjPP`S&w^9ir^sNm0z zh!?^*BKE}9DLnfIGj&J-5qORt5!erJGNpIgZbOAawFMMBUv!efy8AMKz7tCGHO>c0 zL;|Attu|BXtc|0vZ^f=H$A^ysLF^$w1}v94=?soqrE7YSSiB(Kl*BFXx2m+HoV1c?8D+IPVqn_$Nr5fWr09 zWv_V!Y8w(G!@M~&om7!VChu#c`tDMiXGCgK+Dx~Yxw5bDbAgwQ$q6gCcJ8_rh4WTs z;3Z%{=GgS$>RZ>#EGbxvA%ixC=XlokHp(;_MsHA_D{u}l8!7dtPuefWt+=wU1Fap) z2M}1z`Kt$L$pZ6=cpbcxPHGIy$VSL|`&!QQ9CwC3oEfK7)VrY1b_H61EMPm^zV_0$ zVS4^Gv>Ltjt^yu5Wb(sxU6lc+ZuJsU{30l>N?TNDN%pOz^*+> zm%d(2lThv>_%oEhy`YLj7-TQVu$Rh}-^t|nJf0Jn%r?oW-T4E-)&Q!*E`w zzyB`tg@I1`C=bN;@#3D@{nqXkF>`17XWuYlaQbbosFLG_-J@tAv)@h$lA?pX!Igv1 zQ*}AV$OaIDxxJOcqbU!22aBY&+><}fT;rMoqSPBumDvM%A1r|DUYl3+I8wMx59Wg} z`oH!lFSpSiRLht0Gidzk?t5KbVA}s4`o4B00p3_`5*Yjb2jGDk7HAN(6<5PTmKWoN zo5mOI7j#hykWh(C6rbPrOb_aPj%D)-zjNdbq z|HhQA`o%CWe`KSzzTQp=#fPRMO{>{Ajs`hxwPP)B?p63pM8kO^xH_D>)Y&{jIjzje zvc7Uc57j!9vzbOaygMM&4wXk3@$IvH)RO0vIF^P%DDTIn3=G$=$6O_G@jEDsdS;bPHzUXHV!@>C=ps~e3%+n;M@Gvsn!T=QzT%H6ZXbQH|@7jTrT0J zs_biAH)fUP5U$c1&|rxb&-0^pBRKJ>BTgN(sBSih3@ny;Pp2GYdZ1cSer_)yl54eRkL6g#p)I?$XEmw% zxrB6DgLojCsjC_2Kik}`iw!%r@`9Rj$h6aPt51zX^D0*Rtc!;s+AQ0)U)$vcVNPRp zUpIy4Tka>T;WN2q9(6wmXBg*+f(m03nPCZ69*_qxVugyp#<}xPKIEmijN0eQ1-#JFRXUxEW2b!)mH8D52+PKM=o}t5&7@{k7*3Z60 z@7V+}K7721H6QgpyA44X>X5dSG63i8U`edK9{{*^AhZl!jBE*VJ{Pmyo_@O4(K0#%J!)^_F-A1Y0bf+Kb)NYa1m`eDJt%i z=xQ+sX2r1$iFvnnNx+}}-O`AxY2W+P>P5gEl+S`W|C7y zj)uFXBI&6|atmzrIA?1stL*eu%eE(V5Ii<}>bj1*1e^%d0H1CAtyc~zy`W;JRg)p< wv6HHCuw!D^&Z)bCWsSS;-`@Yd_Smo!hj%Cna}_GUXF?E^!%<}YA`). +- **Algorithm Efficiency:** Enhances the performance of data mining and machine learning algorithms by providing a simplified representation of the dataset. +- **Outlier Management:** Efficiently mitigates the effect of outliers by grouping them into the extreme bins, thus preserving the integrity of the main data distribution. +- **Data Smoothing:** Helps smooth the data, reduces noise, and improves the model's ability to generalize. -Let's load the house prices dataset and separate it into train and test sets: +Limitations +~~~~~~~~~~~ + +On the other hand, :class:`EqualWidthDiscretiser()` can lead to a loss of information by aggregating data into broader categories. This is particularly concerning if the data in the same bin has predictive information about the target. + +Let's consider a binary classifier task using a decision tree model. A bin with a high proportion of both categories would potentially impact the model's performance in this scenario. + +Notes +----- + +`EqualWidthDiscretiser` expects a `pandas.DataFrame` and works only with numerical variables. The user can specify the variables to be discretized. Otherwise, `EqualWidthDiscretiser` will automatically infer the data types to compute the interval limits for all numeric variables. + +**Optimal number of intervals:** With `EqualWidthDiscretiser`, the user defines the number of bins. Smaller intervals may be required if the variable is highly skewed or not continuous. Otherwise, the transformer will introduce `numpy.nan`. + +**Integration with scikit-learn:** `EqualFrequencyDiscretiser` and all other feature-engine transformers seamlessly integrate with scikit-learn [pipelines](https://scikit-learn.org/stable/modules/generated/sklearn.pipeline.Pipeline.html) and [column transformers](https://scikit-learn.org/stable/modules/generated/sklearn.compose.ColumnTransformer.html). + +Python code example +------------------- + +Load dataset +~~~~~~~~~~~~ + +In this example, we'll use the House Prices' Dataset (for more details, please check [this link](https://www.openml.org/search?type=data&sort=version&status=any&order=asc&exact_name=house_prices)). + +First, let's load the dataset and split it into train and test sets: .. code:: python - import numpy as np - import pandas as pd import matplotlib.pyplot as plt + from sklearn.datasets import fetch_openml from sklearn.model_selection import train_test_split - from feature_engine.discretisation import EqualWidthDiscretiser + from feature_engine.discretisation import EqualFrequencyDiscretiser # Load dataset - data = data = pd.read_csv('houseprice.csv') + X, y = fetch_openml(name='house_prices', version=1, return_X_y=True, as_frame=True) + X.set_index('Id', inplace=True) # Separate into train and test sets - X_train, X_test, y_train, y_test = train_test_split( - data.drop(['Id', 'SalePrice'], axis=1), - data['SalePrice'], test_size=0.3, random_state=0) + X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42) -Now we want to discretise the 2 variables indicated below into 10 intervals of equal -width: +Equal-width Discretisation +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In this example, let's discretize two variables (LotArea and GrLivArea) into 10 intervals of equal width: .. code:: python - # set up the discretisation transformer - disc = EqualWidthDiscretiser(bins=10, variables=['LotArea', 'GrLivArea']) + # List the target numeric variables for equal-width discretization + TARGET_NUMERIC_FEATURES= ['LotArea','GrLivArea'] - # fit the transformer - disc.fit(X_train) + # Set up the discretisation transformer + disc = EqualWidthDiscretiser(bins=10, variables=TARGET_NUMERIC_FEATURES) -With `fit()` the transformer learns the boundaries of each interval. Then, we can go -ahead and sort the values into the intervals: + # Fit the transformer + disc.fit(X_train) -.. code:: python - # transform the data - train_t= disc.transform(X_train) - test_t= disc.transform(X_test) +Note that if we do not specify the variables (default=`None`), `EqualWidthDiscretiser` will automatically infer the data types to compute the interval limits for all numeric variables. -The `binner_dict_` stores the interval limits identified for each variable. +With the `.fit()` method, the discretiser learns the bin boundaries and saves them into a dictionary so we can use them to transform unseen data: .. code:: python + # Learnt limits for each variable disc.binner_dict_ + .. code:: python - 'LotArea': [-inf, + {'LotArea': [-inf, 22694.5, 44089.0, 65483.5, @@ -87,41 +108,135 @@ The `binner_dict_` stores the interval limits identified for each variable. 193850.5, inf], 'GrLivArea': [-inf, - 768.2, - 1202.4, - 1636.6, - 2070.8, - 2505.0, - 2939.2, - 3373.4, - 3807.6, - 4241.799999999999, + 864.8, + 1395.6, + 1926.3999999999999, + 2457.2, + 2988.0, + 3518.7999999999997, + 4049.5999999999995, + 4580.4, + 5111.2, inf]} -With equal width discretisation, each bin does not necessarily contain the same number of observations. + +Note that the lower and upper boundariers are set to -inf and inf, respectively. This behavior ensures the transformer works even for unseen limits (lower than the minimum or greater than the maximum trained value). + +Also, this transformer will not work in the presence of missing values. Therefore, we should either remove or impute missing values before fitting the transformer. + +.. code:: python + + # Transform the data (data discretization) + train_t = disc.transform(X_train) + test_t = disc.transform(X_test) + +Let's visualize the first rows of the raw data and the transformed data: + +.. code:: python + + # Raw data + print(X_train[TARGET_NUMERIC_FEATURES].head()) + + +.. code:: python + + LotArea GrLivArea + Id + 136 10400 1682 + 1453 3675 1072 + 763 8640 1547 + 933 11670 1905 + 436 10667 1661 + + +.. code:: python + + # Transformed data + print(train_t[TARGET_NUMERIC_FEATURES].head()) + + +.. code:: python + + LotArea GrLivArea + Id + 136 0 2 + 1453 0 1 + 763 0 2 + 933 0 2 + 436 0 2 + + +The transformed data now contains discrete values corresponding to the ordered computed buckets (0 being the first and bins-1 the last). + +Now, let's visualize its output: .. code:: python - train_t.groupby('GrLivArea')['GrLivArea'].count().plot.bar() + train_t['GrLivArea'].value_counts().sort_index().plot.bar() plt.ylabel('Number of houses') + plt.show() -We can see below that the intervals contain different number of observations. .. image:: ../../images/equalwidthdiscretisation.png -| +As we can see, the intervals contain different number of observations. +It's a similar output of an histogram, but it's designed to work transform unseen data, includig outliers. + +Finally, since the default value for the `return_object` parameter is `False`, the transformer outputs integer variables: + +.. code:: python + + train_t[TARGET_NUMERIC_FEATURES].dtypes + + +.. code:: python + + LotArea int64 + GrLivArea int64 + dtype: object + + +Return object instead of integers +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Categorical encoders in feature-engine are designed to work by default with variables of type object. Therefore, to further encode the discretiser output with feature-engine, we can set `return_object=True` instead. This will return the transformed variables as object. + +Let's say we want to obtain monotonic relationships between the variable and the target. We can do that seamlessly by setting `return_object` to True. A tutorial of how to use this functionality is available [here](https://nbviewer.org/github/feature-engine/feature-engine-examples/blob/main/discretisation/EqualWidthDiscretiser_plus_OrdinalEncoder.ipynb). + +Additionally, if we want to output the intervals as object while specifying the boundaries, we can set `return_boundaries` to True: + +.. code:: python + + # Set up the discretisation transformer + disc = EqualFrequencyDiscretiser(bins=10, variables=TARGET_NUMERIC_FEATURES, return_boundaries=True) + + # Fit the transformer + disc.fit(X_train) + + # Transform test set & visualize limit + test_t = disc.transform(X_test) + + # Visualize output (boundaries) + print(test_t[TARGET_NUMERIC_FEATURES].head()) + + +.. code:: python + + LotArea GrLivArea + Id + 893 (-inf, 22694.5] (864.8, 1395.6] + 1106 (-inf, 22694.5] (2457.2, 2988.0] + 414 (-inf, 22694.5] (864.8, 1395.6] + 523 (-inf, 22694.5] (1395.6, 1926.4] + 1037 (-inf, 22694.5] (1395.6, 1926.4] -**Discretisation plus encoding** -If we return the interval values as integers, the discretiser has the option to return -the transformed variable as integer or as object. Why would we want the transformed -variables as object? +See Also +-------- -Categorical encoders in Feature-engine are designed to work with variables of type -object by default. Thus, if you wish to encode the returned bins further, say to try and -obtain monotonic relationships between the variable and the target, you can do so -seamlessly by setting `return_object` to True. You can find an example of how to use -this functionality `here `_. +- Further feature-engine discretiser / binning methods[here](https://feature-engine.trainindata.com/en/latest/user_guide/discretisation/index.html) +- Scikit-learn [`KBinsDiscretizer`](https://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.KBinsDiscretizer.html#sklearn.preprocessing.KBinsDiscretizer) class +- [Pandas cut](https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.cut.html) Additional resources -------------------- From 482f2599b27b40cc65875093017d7d21f72addeb Mon Sep 17 00:00:00 2001 From: Soledad Galli Date: Thu, 25 Apr 2024 10:58:34 +0200 Subject: [PATCH 4/7] reworded equal width copy --- .../discretisation/EqualWidthDiscretiser.rst | 130 ++++++++++++------ docs/user_guide/discretisation/index.rst | 2 + 2 files changed, 90 insertions(+), 42 deletions(-) diff --git a/docs/user_guide/discretisation/EqualWidthDiscretiser.rst b/docs/user_guide/discretisation/EqualWidthDiscretiser.rst index 7e1efd456..ea9854c5a 100644 --- a/docs/user_guide/discretisation/EqualWidthDiscretiser.rst +++ b/docs/user_guide/discretisation/EqualWidthDiscretiser.rst @@ -5,17 +5,24 @@ EqualWidthDiscretiser ===================== -The :class:EqualWidthDiscretiser() divides continuous numerical variables into intervals of equal width, calculated using the formula: +Equal width discretization consist of dividing continuous variables into intervals of equal width, calculated +using the following formula: -( max(X) - min(X) ) / bins +.. math:: -where bins is the number of intervals specified by the user. This discretisation is achieved through the pandas.cut() function. + bin_{width} = ( max(X) - min(X) ) / bins -Discretization is a common data preprocessing technique used in data science. It's also known as data binning (or simply `binning`). +Here, `bins` is the number of intervals specified by the user and `max(X)` and `min(X)` are the minimum and maximum values +of the variable to discretize. + +Discretization is a common data preprocessing technique used in data science. It's also known as data binning (or simply +"binning"). Advantages and Limitations -------------------------- +Equal binning discretization has some advantages and also shortcomings. + Advantages ~~~~~~~~~~ @@ -28,18 +35,28 @@ Some advantagers of equal width binning: Limitations ~~~~~~~~~~~ -On the other hand, :class:`EqualWidthDiscretiser()` can lead to a loss of information by aggregating data into broader categories. This is particularly concerning if the data in the same bin has predictive information about the target. +On the other hand, equal width discretzation can lead to a loss of information by aggregating data into broader categories. +This is particularly concerning if the data in the same bin has predictive information about the target. -Let's consider a binary classifier task using a decision tree model. A bin with a high proportion of both categories would potentially impact the model's performance in this scenario. +Let's consider a binary classifier task using a decision tree model. A bin with a high proportion of both target categories would +potentially impact the model's performance in this scenario. + +EqualWidthDiscretiser +--------------------- -Notes ------ +Feture-engine's :class:`EqualWidthDiscretiser()` applies equal width discretization to numerical variables. It uses +the `pandas.cut()` function under the hood to find the interval limits and then sort the continuous variables into +the bins. -`EqualWidthDiscretiser` expects a `pandas.DataFrame` and works only with numerical variables. The user can specify the variables to be discretized. Otherwise, `EqualWidthDiscretiser` will automatically infer the data types to compute the interval limits for all numeric variables. +You can specify the variables to be discretized by passing their names in a list when you set up the transformer. Alternatively, +:class:`EqualWidthDiscretiser()` will automatically infer the data types to compute the interval limits for all numeric +variables. -**Optimal number of intervals:** With `EqualWidthDiscretiser`, the user defines the number of bins. Smaller intervals may be required if the variable is highly skewed or not continuous. Otherwise, the transformer will introduce `numpy.nan`. +**Optimal number of intervals:** With :class:`EqualWidthDiscretiser()`, the user defines the number of bins. Smaller intervals +may be required if the variable is highly skewed or not continuous. Otherwise, the transformer will introduce `numpy.nan`. -**Integration with scikit-learn:** `EqualFrequencyDiscretiser` and all other feature-engine transformers seamlessly integrate with scikit-learn [pipelines](https://scikit-learn.org/stable/modules/generated/sklearn.pipeline.Pipeline.html) and [column transformers](https://scikit-learn.org/stable/modules/generated/sklearn.compose.ColumnTransformer.html). +**Integration with scikit-learn:** :class:`EqualWidthDiscretiser()` and all other Feature-engine transformers seamlessly +integrate with scikit-learn `pipelines `_. Python code example ------------------- @@ -47,9 +64,8 @@ Python code example Load dataset ~~~~~~~~~~~~ -In this example, we'll use the House Prices' Dataset (for more details, please check [this link](https://www.openml.org/search?type=data&sort=version&status=any&order=asc&exact_name=house_prices)). - -First, let's load the dataset and split it into train and test sets: +In this example, we'll use the Ames House Prices' Dataset. First, let's load the dataset and split it into train and +test sets: .. code:: python @@ -67,30 +83,32 @@ First, let's load the dataset and split it into train and test sets: X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42) -Equal-width Discretisation +Equal-width Discretization ~~~~~~~~~~~~~~~~~~~~~~~~~~ -In this example, let's discretize two variables (LotArea and GrLivArea) into 10 intervals of equal width: +In this example, let's discretize two variables, LotArea and GrLivArea, into 10 intervals of equal width: .. code:: python # List the target numeric variables for equal-width discretization TARGET_NUMERIC_FEATURES= ['LotArea','GrLivArea'] - # Set up the discretisation transformer + # Set up the discretization transformer disc = EqualWidthDiscretiser(bins=10, variables=TARGET_NUMERIC_FEATURES) # Fit the transformer disc.fit(X_train) -Note that if we do not specify the variables (default=`None`), `EqualWidthDiscretiser` will automatically infer the data types to compute the interval limits for all numeric variables. +Note that if we do not specify the variables (default=`None`), :class:`EqualWidthDiscretiser` will automatically infer +the data types to compute the interval limits for all numeric variables. -With the `.fit()` method, the discretiser learns the bin boundaries and saves them into a dictionary so we can use them to transform unseen data: +With the `fit()` method, the discretizer learns the bin boundaries and saves them into a dictionary so we can use them +to transform unseen data: .. code:: python - # Learnt limits for each variable + # Learned limits for each variable disc.binner_dict_ @@ -120,9 +138,12 @@ With the `.fit()` method, the discretiser learns the bin boundaries and saves th inf]} -Note that the lower and upper boundariers are set to -inf and inf, respectively. This behavior ensures the transformer works even for unseen limits (lower than the minimum or greater than the maximum trained value). +Note that the lower and upper boundaries are set to -inf and inf, respectively. This behavior ensures that the transformer +will be able to allocate to the extreme bins values that are smaller or greater than the observed minimum and maximum +values in the training set. -Also, this transformer will not work in the presence of missing values. Therefore, we should either remove or impute missing values before fitting the transformer. +:class:`EqualWidthDiscretiser` will not work in the presence of missing values. Therefore, we should either remove or +impute missing values before fitting the transformer. .. code:: python @@ -137,6 +158,7 @@ Let's visualize the first rows of the raw data and the transformed data: # Raw data print(X_train[TARGET_NUMERIC_FEATURES].head()) +Here we see the original variables: .. code:: python @@ -154,6 +176,7 @@ Let's visualize the first rows of the raw data and the transformed data: # Transformed data print(train_t[TARGET_NUMERIC_FEATURES].head()) +Here we observe the variables after discretization: .. code:: python @@ -166,9 +189,10 @@ Let's visualize the first rows of the raw data and the transformed data: 436 0 2 -The transformed data now contains discrete values corresponding to the ordered computed buckets (0 being the first and bins-1 the last). +The transformed data now contains discrete values corresponding to the ordered computed buckets (0 being the first and +bins-1 the last). -Now, let's visualize its output: +Now, let's check out the number of observations per bin by creating a bar plot: .. code:: python @@ -176,11 +200,15 @@ Now, let's visualize its output: plt.ylabel('Number of houses') plt.show() +As we see in the following image, the intervals contain different number of observations. It's a similar output to a +histogram: .. image:: ../../images/equalwidthdiscretisation.png -As we can see, the intervals contain different number of observations. -It's a similar output of an histogram, but it's designed to work transform unseen data, includig outliers. +| + +Equal width discretization does not improve the spread of values over the value range. If the variable is skewed, it will +still be skewed after the discretization. Finally, since the default value for the `return_object` parameter is `False`, the transformer outputs integer variables: @@ -196,29 +224,42 @@ Finally, since the default value for the `return_object` parameter is `False`, t dtype: object -Return object instead of integers -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Return variables as object +~~~~~~~~~~~~~~~~~~~~~~~~~~ -Categorical encoders in feature-engine are designed to work by default with variables of type object. Therefore, to further encode the discretiser output with feature-engine, we can set `return_object=True` instead. This will return the transformed variables as object. +Categorical encoders in Feature-engine are designed to work by default with variables of type object. Therefore, to +further encode the discretized output with Feature-engine's encoders, we can set `return_object=True` instead. This will +return the transformed variables as object. -Let's say we want to obtain monotonic relationships between the variable and the target. We can do that seamlessly by setting `return_object` to True. A tutorial of how to use this functionality is available [here](https://nbviewer.org/github/feature-engine/feature-engine-examples/blob/main/discretisation/EqualWidthDiscretiser_plus_OrdinalEncoder.ipynb). +Let's say we want to obtain monotonic relationships between the variable and the target. We can do that seamlessly by +setting `return_object` to `True`. A tutorial of how to use this functionality is available +`here `_. -Additionally, if we want to output the intervals as object while specifying the boundaries, we can set `return_boundaries` to True: +Return bin boundaries +~~~~~~~~~~~~~~~~~~~~~ + +If we want to output the intervals limites instead of integers, we can set `return_boundaries` to `True`: .. code:: python - # Set up the discretisation transformer - disc = EqualFrequencyDiscretiser(bins=10, variables=TARGET_NUMERIC_FEATURES, return_boundaries=True) + # Set up the discretization transformer + disc = EqualFrequencyDiscretiser( + bins=10, + variables=TARGET_NUMERIC_FEATURES, + return_boundaries=True) - # Fit the transformer - disc.fit(X_train) + # Fit the transformer + disc.fit(X_train) - # Transform test set & visualize limit - test_t = disc.transform(X_test) + # Transform test set & visualize limit + test_t = disc.transform(X_test) - # Visualize output (boundaries) - print(test_t[TARGET_NUMERIC_FEATURES].head()) + # Visualize output (boundaries) + print(test_t[TARGET_NUMERIC_FEATURES].head()) +In the following output we see that the transformed variables now display the interval limits. While we can't use these +variables to train machine learning models, as opposed to the variables discretized into integers, they are very useful +in this format for data analysis, and they can also be passed on to any Feature-engine encoder for further processing. .. code:: python @@ -234,9 +275,14 @@ Additionally, if we want to output the intervals as object while specifying the See Also -------- -- Further feature-engine discretiser / binning methods[here](https://feature-engine.trainindata.com/en/latest/user_guide/discretisation/index.html) -- Scikit-learn [`KBinsDiscretizer`](https://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.KBinsDiscretizer.html#sklearn.preprocessing.KBinsDiscretizer) class -- [Pandas cut](https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.cut.html) +For alternative binning techniques, check out the following resources: + +- Further feature-engine :ref:`discretizers / binning methods ` +- Scikit-learn's `KBinsDiscretizer `_. + +Check out also: + +- `Pandas cut `_. Additional resources -------------------- diff --git a/docs/user_guide/discretisation/index.rst b/docs/user_guide/discretisation/index.rst index 9a7fc91c6..559dc751a 100644 --- a/docs/user_guide/discretisation/index.rst +++ b/docs/user_guide/discretisation/index.rst @@ -1,3 +1,5 @@ +.. _discretization_transformers: + .. -*- mode: rst -*- Discretisation From 5ee233497be8764cbd4577df0a28e324e3c10d5f Mon Sep 17 00:00:00 2001 From: Soledad Galli Date: Thu, 25 Apr 2024 11:34:41 +0200 Subject: [PATCH 5/7] re worded equal frequency class --- .../EqualFrequencyDiscretiser.rst | 177 ++++++++++++------ .../discretisation/EqualWidthDiscretiser.rst | 6 +- 2 files changed, 121 insertions(+), 62 deletions(-) diff --git a/docs/user_guide/discretisation/EqualFrequencyDiscretiser.rst b/docs/user_guide/discretisation/EqualFrequencyDiscretiser.rst index 157fe292a..5fa9d6ce2 100644 --- a/docs/user_guide/discretisation/EqualFrequencyDiscretiser.rst +++ b/docs/user_guide/discretisation/EqualFrequencyDiscretiser.rst @@ -5,49 +5,67 @@ EqualFrequencyDiscretiser ========================= -The :class:`EqualFrequencyDiscretiser()` is a discretization method that applies the pandas.qcut() function to divide continuous numerical variables into equal-frequency bins. These bins contain roughly the same number of observations, with boundaries set at specific quantile values determined by the desired number of bins (q parameter). This method ensures a uniform distribution of data points across the range of values, enhancing the handling of skewed data and outliers. +Equal frequency discretization consists of dividing continuous attributes into equal-frequency bins. These bins +contain roughly the same number of observations, with boundaries set at specific quantile values determined by the desired +number of bins. -Discretization is a common data preprocessing technique used in data science. It's also known as binning data (or simply `binning`). +Equal frequency discretization ensures a uniform distribution of data points across the range of values, enhancing the +handling of skewed data and outliers. + +Discretization is a common data preprocessing technique used in data science. It's also known as binning data (or simply "binning"). Advantages and Limitations -------------------------- +Equal frequency discretization has some advantages and shortcomings: + Advantages ~~~~~~~~~~ -:class:`EqualFrequencyDiscretiser()` has the same advantages as the classic equal width discretizer: +Some advantages of equal frequency binning: - **Algorithm Efficiency:** Enhances the performance of data mining and machine learning algorithms by providing a simplified representation of the dataset. -- **Outlier Management:** Efficiently mitigates the effect of outliers by grouping them into the extreme bins, thus preserving the integrity of the main data distribution. +- **Outlier Management:** Efficiently mitigates the effect of outliers by grouping them into the extreme bins. - **Data Smoothing:** Helps smooth the data, reduces noise, and improves the model's ability to generalize. +- **Improved value distribution:** Returns an uniform distribution of values across the value range. -Plus, it improves the data distribution, **optimizing the spread of values**. This is particularly beneficial in datasets with skewed distributions (see the Python example code). +Equal frequency discretization improves the data distribution, **optimizing the spread of values**. This is particularly +beneficial for datasets with skewed distributions (see the Python example code). Limitations ~~~~~~~~~~~ -On the other hand, :class:`EqualFrequencyDiscretiser()` can lead to a loss of information by aggregating data into broader categories. This is particularly concerning if the data in the same bin has predictive information about the target. +On the other hand, equal frequency binning can lead to a loss of information by aggregating data into broader categories. +This is particularly concerning if the data in the same bin has predictive information about the target. + +Let's consider a binary classifier task using a decision tree model. A bin with a high proportion of both target categories +would potentially impact the model's performance in this scenario. -Let's consider a binary classifier task using a decision tree model. A bin with a high proportion of both categories would potentially impact the model's performance in this scenario. +EqualFrequencyDiscretiser +------------------------- -Notes ------ +Feature-engine's :class:`EqualFrequencyDiscretiser` applies equal frequency discretization to numerical variables. It uses +the `pandas.qcut()` function under the hood, to determine the interval limits. -`EqualFrequencyDiscretiser` expects a `pandas.DataFrame` and works only with numerical variables. The user can specify the variables to be discretized. Otherwise, `EqualFrequencyDiscretiser` will automatically infer the data types to compute the interval limits for all numeric variables. +You can specify the variables to be discretized by passing their names in a list when you set up the transformer. Alternatively, +:class:`EqualFrequencyDiscretiser` will automatically infer the data types to compute the interval limits for all numeric variables. -**Optimal number of intervals:** With `EqualFrequencyDiscretiser`, the user defines the number of bins. Smaller intervals may be required if the variable is highly skewed or not continuous. Otherwise, the transformer will introduce `numpy.nan`. +**Optimal number of intervals:** With :class:`EqualFrequencyDiscretiser`, the user defines the number of bins. Smaller intervals +may be required if the variable is highly skewed or not continuous. -**Integration with scikit-learn:** `EqualFrequencyDiscretiser` and all other feature-engine transformers seamlessly integrate with scikit-learn [pipelines](https://scikit-learn.org/stable/modules/generated/sklearn.pipeline.Pipeline.html) and [column transformers](https://scikit-learn.org/stable/modules/generated/sklearn.compose.ColumnTransformer.html). +**Integration with scikit-learn:** :class:`EqualFrequencyDiscretiser` and all other feature-engine transformers seamlessly integrate +with scikit-learn `pipelines `_. Python code example ------------------- +In this section, we'll show the main functionality of :class:`EqualFrequencyDiscretiser` + Load dataset ~~~~~~~~~~~~ -In this example, we'll use the House Prices' Dataset (for more details, please check [this link](https://www.openml.org/search?type=data&sort=version&status=any&order=asc&exact_name=house_prices)). - -First, let's load the dataset and split it into train and test sets: +In this example, we'll use the Ames House Prices' Dataset. First, let's load the dataset and split it into train and +test sets: .. code:: python @@ -65,30 +83,33 @@ First, let's load the dataset and split it into train and test sets: X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42) -Equal-frequency Discretisation +Equal-frequency Discretization ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -In this example, let's discretize two variables (LotArea and GrLivArea) into 10 intervals of approximately equal number of observations. +In this example, let's discretize two variables, LotArea and GrLivArea, into 10 intervals of approximately equal +number of observations. .. code:: python # List the target numeric variables to be transformed TARGET_NUMERIC_FEATURES= ['LotArea','GrLivArea'] - # Set up the discretisation transformer + # Set up the discretization transformer disc = EqualFrequencyDiscretiser(q=10, variables=TARGET_NUMERIC_FEATURES) # Fit the transformer disc.fit(X_train) -Note that if we do not specify the variables (default=`None`), `EqualFrequencyDiscretiser` will automatically infer the data types to compute the interval limits for all numeric variables. +Note that if we do not specify the variables (default=`None`), :class:`EqualFrequencyDiscretiser` will automatically +infer the data types to compute the interval limits for all numeric variables. -With the `.fit()` method, the discretiser learns the bin boundaries and saves them into a dictionary so we can use them to transform unseen data: +With the `fit()` method, the discretizer learns the bin boundaries and saves them into a dictionary so we can use them +to transform unseen data: .. code:: python - # Learnt limits for each variable + # Learned limits for each variable disc.binner_dict_ @@ -118,9 +139,12 @@ With the `.fit()` method, the discretiser learns the bin boundaries and saves th inf]} -Note that the lower and upper boundariers are set to -inf and inf, respectively. This behavior ensures the transformer works even for unseen limits (lower than the minimum or greater than the maximum trained value). +Note that the lower and upper boundaries are set to -inf and inf, respectively. his behavior ensures that the transformer +will be able to allocate to the extreme bins values that are smaller or greater than the observed minimum and maximum +values in the training set. -Also, this transformer will not work in the presence of missing values. Therefore, we should either remove or impute missing values before fitting the transformer. +:class:`EqualFrequencyDiscretiser` will not work in the presence of missing values. Therefore, we should either remove or +impute missing values before fitting the transformer. .. code:: python @@ -129,28 +153,32 @@ Also, this transformer will not work in the presence of missing values. Therefor test_t = disc.transform(X_test) -Let's visualize the first rows of the raw data and the transformed data: +Let's visualize the first rows of the raw data and the transformed data: .. code:: python # Raw data print(X_train[TARGET_NUMERIC_FEATURES].head()) +Here we see the original variables: + .. code:: python - LotArea GrLivArea - Id - 136 10400 1682 - 1453 3675 1072 - 763 8640 1547 - 933 11670 1905 - 436 10667 1661 + LotArea GrLivArea + Id + 136 10400 1682 + 1453 3675 1072 + 763 8640 1547 + 933 11670 1905 + 436 10667 1661 .. code:: python # Transformed data print(train_t[TARGET_NUMERIC_FEATURES].head()) +Here we observe the variables after discretization: + .. code:: python LotArea GrLivArea @@ -164,7 +192,7 @@ Let's visualize the first rows of the raw data and the transformed data: The transformed data now contains discrete values corresponding to the ordered computed buckets (0 being the first and q-1 the last). -Now, let's visualize the plots for equal-width intervals (a common histogram) and the transformed data with equal-frequency discretiser: +Now, let's visualize the plots for equal-width intervals with a histogram and the transformed data with equal-frequency discretiser: .. code:: python @@ -183,12 +211,12 @@ Now, let's visualize the plots for equal-width intervals (a common histogram) an plt.tight_layout(w_pad=2) plt.show() +As we see in the following image, the intervals contain approximately the same number of observations: .. image:: ../../images/equalfrequencydiscretisation_gaussian.png -As we can see, the intervals contain approximately the same number of observations. -Finally, since the default value for the `return_object` parameter is `False`, the transformer outputs integer variables: +Finally, as the default value for the `return_object` parameter is `False`, the transformer outputs integer variables: .. code:: python @@ -202,33 +230,49 @@ Finally, since the default value for the `return_object` parameter is `False`, t dtype: object -Return object instead of integers -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Return variables as object +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Categorical encoders in Feature-engine are designed to work by default with variables of type object. Therefore, to further +encode the discretised output with Feature-engine, we can set `return_object=True` instead. This will return the transformed +variables as object. -Categorical encoders in feature-engine are designed to work by default with variables of type object. Therefore, to further encode the discretiser output with feature-engine, we can set `return_object=True` instead. This will return the transformed variables as object. +Let's say we want to obtain monotonic relationships between the variable and the target. We can do that seamlessly by setting +`return_object` to True. A tutorial of how to use this functionality is available +`here `_. -Let's say we want to obtain monotonic relationships between the variable and the target. We can do that seamlessly by setting `return_object` to True. A tutorial of how to use this functionality is available [here](https://nbviewer.org/github/feature-engine/feature-engine-examples/blob/main/discretisation/EqualFrequencyDiscretiser_plus_WoEEncoder.ipynb). +Return bin boundaries +~~~~~~~~~~~~~~~~~~~~~ -Additionally, if we want to output the intervals as object while specifying the boundaries, we can set `return_boundaries` to True: +If we want to output the intervals limits instead of integers, we can set `return_boundaries` to `True`: .. code:: python - # Set up the discretisation transformer - disc = EqualFrequencyDiscretiser(q=10, variables=TARGET_NUMERIC_FEATURES, return_boundaries=True) + # Set up the discretization transformer + disc = EqualFrequencyDiscretiser( + q=10, + variables=TARGET_NUMERIC_FEATURES, + return_boundaries=True) - # Fit the transformer - disc.fit(X_train) + # Fit the transformer + disc.fit(X_train) - # Transform test set & visualize limit - test_t = disc.transform(X_test) + # Transform test set & visualize limit + test_t = disc.transform(X_test) + + # Visualize output (boundaries) + print(test_t[TARGET_NUMERIC_FEATURES].head()) - # Visualize output (boundaries) - print(test_t[TARGET_NUMERIC_FEATURES].head()) +The transformed variables now show the interval limits in the output. We can immediately see that the bin width for these +intervals varies. In other words, they don't have the same width, contrarily to what we see with :ref:`equal width discretization `. +Unlike the variables discretized into integers, these variables cannot be used to train machine learning models; however, +they are still highly helpful for data analysis in this format, and they may be sent to any Feature-engine encoder for +additional processing. .. code:: python - LotArea GrLivArea + LotArea GrLivArea Id 893 (8099.2, 8874.0] (918.5, 1080.4] 1106 (12208.2, 14570.7] (2166.4, inf] @@ -238,7 +282,10 @@ Additionally, if we want to output the intervals as object while specifying the Binning skewed data -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~ + +Let's now show the benefits of equal frequency discretization for skewed variables. We'll +start by importing the libraries and classes: .. code:: python @@ -247,6 +294,8 @@ Binning skewed data import matplotlib.pyplot as plt from feature_engine.discretisation import EqualFrequencyDiscretiser +Now, we'll create a toy dataset with a variable that is normally distributed and another +one that is skewed: .. code:: python @@ -262,21 +311,21 @@ Binning skewed data # Create dataframe with simulated data X = pd.DataFrame({'feature1': normal_data, 'feature2': skewed_data}) +Let's discretize both variables into 5 equal frequency bins: .. code:: python - # Instantiate discretiser + # Instantiate discretizer disc = EqualFrequencyDiscretiser(q=5) # Transform simulated data X_transformed = disc.fit_transform(X) +Let's plot the original distribution and the distribution after discretization for the variable that was normally +distributed: .. code:: python - # Plot raw and discretized data for normally distributed data - # binning method = equal width vs equal frequency - fig, axes = plt.subplots(1, 2, figsize=(12, 4)) axes[0].hist(X.feature1, bins=disc.q) @@ -289,13 +338,14 @@ Binning skewed data plt.show() +In the following image, we see that after the discretization there is an even distribution of the values across +the value range, hence, the variable does no look normally distributed any more. .. image:: ../../images/equalfrequencydiscretisation_gaussian.png -.. code:: python +Let's now plot the original distribution and the distribution after discretization for the variable that was skewed: - # Plot raw and discretized data for skewed distributed data - # binning method = equal width vs equal frequency +.. code:: python fig, axes = plt.subplots(1, 2, figsize=(12, 4)) @@ -309,15 +359,22 @@ Binning skewed data plt.show() +In the following image, we see that after the discretization there is an even distribution of the values across +the value range. .. image:: ../../images/equalfrequencydiscretisation_skewed.png See Also -------- -- Further feature-engine discretiser / binning options [here](https://feature-engine.trainindata.com/en/latest/user_guide/discretisation/index.html) -- Scikit-learn [`KBinsDiscretizer`](https://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.KBinsDiscretizer.html#sklearn.preprocessing.KBinsDiscretizer) class -- [Pandas qcut](https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.qcut.html) +For alternative binning techniques, check out the following resources: + +- Further feature-engine :ref:`discretizers / binning methods ` +- Scikit-learn's `KBinsDiscretizer `_. + +Check out also: + +- `Pandas cut `_. Additional resources @@ -326,7 +383,7 @@ Additional resources Check also for more details on how to use this transformer: - `Jupyter notebook `_ -- `Jupyter notebook - Discretiser plus Weight of Evidence encoding `_ +- `Jupyter notebook - Discretizer plus Weight of Evidence encoding `_ For more details about this and other feature engineering methods check out these resources: diff --git a/docs/user_guide/discretisation/EqualWidthDiscretiser.rst b/docs/user_guide/discretisation/EqualWidthDiscretiser.rst index ea9854c5a..ced86f6f9 100644 --- a/docs/user_guide/discretisation/EqualWidthDiscretiser.rst +++ b/docs/user_guide/discretisation/EqualWidthDiscretiser.rst @@ -26,7 +26,7 @@ Equal binning discretization has some advantages and also shortcomings. Advantages ~~~~~~~~~~ -Some advantagers of equal width binning: +Some advantages of equal width binning: - **Algorithm Efficiency:** Enhances the performance of data mining and machine learning algorithms by providing a simplified representation of the dataset. - **Outlier Management:** Efficiently mitigates the effect of outliers by grouping them into the extreme bins, thus preserving the integrity of the main data distribution. @@ -61,6 +61,8 @@ integrate with scikit-learn `pipelines Date: Thu, 25 Apr 2024 11:02:50 -0300 Subject: [PATCH 6/7] Update EqualWidthDiscretiser.rst Remove numpy.nan statement and update text from discretiser to discretizer. --- docs/user_guide/discretisation/EqualWidthDiscretiser.rst | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/docs/user_guide/discretisation/EqualWidthDiscretiser.rst b/docs/user_guide/discretisation/EqualWidthDiscretiser.rst index ced86f6f9..81fcb2f5b 100644 --- a/docs/user_guide/discretisation/EqualWidthDiscretiser.rst +++ b/docs/user_guide/discretisation/EqualWidthDiscretiser.rst @@ -52,9 +52,6 @@ You can specify the variables to be discretized by passing their names in a list :class:`EqualWidthDiscretiser()` will automatically infer the data types to compute the interval limits for all numeric variables. -**Optimal number of intervals:** With :class:`EqualWidthDiscretiser()`, the user defines the number of bins. Smaller intervals -may be required if the variable is highly skewed or not continuous. Otherwise, the transformer will introduce `numpy.nan`. - **Integration with scikit-learn:** :class:`EqualWidthDiscretiser()` and all other Feature-engine transformers seamlessly integrate with scikit-learn `pipelines `_. @@ -292,7 +289,7 @@ Additional resources Check also for more details on how to use this transformer: - `Jupyter notebook `_ -- `Jupyter notebook - Discretiser plus Ordinal encoding `_ +- `Jupyter notebook - Discretizer plus Ordinal encoding `_ For more details about this and other feature engineering methods check out these resources: @@ -341,4 +338,4 @@ Or read our book: | Both our book and course are suitable for beginners and more advanced data scientists -alike. By purchasing them you are supporting Sole, the main developer of Feature-engine. \ No newline at end of file +alike. By purchasing them you are supporting Sole, the main developer of Feature-engine. From 9b7938ffe0c8d90c72ae2dfd6cbab92ea341f41b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cain=C3=A3=20Max=20Couto=20da=20Silva?= Date: Thu, 25 Apr 2024 11:37:46 -0300 Subject: [PATCH 7/7] Update EqualWidthDiscretiser.rst Adding back the optimal number of intervals, without the numpy.nan statement. --- docs/user_guide/discretisation/EqualWidthDiscretiser.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/user_guide/discretisation/EqualWidthDiscretiser.rst b/docs/user_guide/discretisation/EqualWidthDiscretiser.rst index 81fcb2f5b..c937d43db 100644 --- a/docs/user_guide/discretisation/EqualWidthDiscretiser.rst +++ b/docs/user_guide/discretisation/EqualWidthDiscretiser.rst @@ -52,6 +52,9 @@ You can specify the variables to be discretized by passing their names in a list :class:`EqualWidthDiscretiser()` will automatically infer the data types to compute the interval limits for all numeric variables. +**Optimal number of intervals:** With :class:`EqualWidthDiscretiser()`, the user defines the number of bins. Smaller intervals +may be required if the variable is highly skewed or not continuous. + **Integration with scikit-learn:** :class:`EqualWidthDiscretiser()` and all other Feature-engine transformers seamlessly integrate with scikit-learn `pipelines `_.