Repository: dirty-cat/dirty_cat
Pull request #469 (2023-01-18 09:49:04)

FIX Remove CI doc build warnings

LeoGrin: Thanks! There are still a few warnings, especially in the FeatureAugmenter and Deduplication docs.

Patch:
diff --git a/.github/ISSUE_TEMPLATE/documentation.md b/.github/ISSUE_TEMPLATE/documentation.md
index ed69f9c..d24e63e 100644
--- a/.github/ISSUE_TEMPLATE/documentation.md
+++ b/.github/ISSUE_TEMPLATE/documentation.md
@@ -10,4 +10,4 @@ assignees: ''
 <!--List pages that would be impacted by the enhancement-->
 
 Affected pages:
-- 
+-
diff --git a/.gitignore b/.gitignore
index dc2445f..746bffc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -64,4 +64,3 @@ doc/CONTRIBUTING.rst
 
 # Pkl files for benchmarks
 benchmarks/*.pkl
-
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 71fc470..58dc8c3 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -3,12 +3,16 @@ repos:
     rev: v4.3.0
     hooks:
     -   id: check-yaml
+        exclude: doc/
     -   id: end-of-file-fixer
+        exclude: doc/
     -   id: trailing-whitespace
+        exclude: doc/
 -   repo: https://github.com/psf/black
     rev: 22.8.0
     hooks:
     -   id: black
+        exclude: doc/
 -   repo: https://github.com/pycqa/flake8
     rev: 5.0.4
     hooks:
@@ -16,7 +20,7 @@ repos:
         files: dirty_cat/
         types: [file, python]
 -   repo: https://github.com/PyCQA/isort
-    rev: 5.10.1
+    rev: 5.12.0
     hooks:
     -   id: isort
         files: dirty_cat/
diff --git a/CHANGES.rst b/CHANGES.rst
index fad8b66..07665d3 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -5,21 +5,21 @@
 Release 0.5.0
 ==============
 
-Release 0.4.0 (upcoming)
-=========================
+Release 0.4.0 (beta)
+====================
 
 Major changes
 -------------
 
 * New experimental feature: joining tables using :func:`fuzzy_join` by approximate key matching. Matches are based
   on string similarities and the nearest neighbors matches are found for each category.
-:pr:`291` by :user:`Jovan Stojanovic <jovan-stojanovic>` and :user:`Leo Grinsztajn <LeoGrin>`
+  :pr:`291` by :user:`Jovan Stojanovic <jovan-stojanovic>` and :user:`Leo Grinsztajn <LeoGrin>`
 * New experimental feature: :class:`FeatureAugmenter`, a transformer
-  that augments with :func:`fuzzy_join` the number of features in a main table
-  by using information from auxilliary tables. :pr:`409` by :user:`Jovan Stojanovic <jovan-stojanovic>`
-* **datasets.fetching**: contains a new function :func:`fetch_world_bank_indicator`
-  that can be used to download any indicator from the World Bank Open Data platform.
-  It only needs the indicator ID that can be found on the website. :pr:`291` by :user:`Jovan Stojanovic <jovan-stojanovic>`
+  that augments with :func:`fuzzy_join` the number of features in a main table by using information from auxilliary tables.
+  :pr:`409` by :user:`Jovan Stojanovic <jovan-stojanovic>`
+* **datasets.fetching**: contains a new function :func:`fetch_world_bank_indicator` that can be used to download any indicator
+  from the World Bank Open Data platform, the indicator ID that can be found there.
+  :pr:`291` by :user:`Jovan Stojanovic <jovan-stojanovic>`
 * Unnecessary API has been made private: everything (files, functions, classes)
   starting with an underscore shouldn't be imported in your code. :pr:`331` by :user:`Lilian Boulard <LilianBoulard>`
 * The :class:`MinHashEncoder` now supports a `n_jobs` parameter to parallelize
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
index 2301656..b4e1709 100644
--- a/CODE_OF_CONDUCT.md
+++ b/CODE_OF_CONDUCT.md
@@ -13,4 +13,3 @@ all priceless contributions.
 
 We abide by the principles of openness, respect, and consideration of others of
 the Python Software Foundation: https://www.python.org/psf/codeofconduct/
-
diff --git a/benchmarks/bench_minhash.py b/benchmarks/bench_minhash.py
index bbffa6f..d891c51 100644
--- a/benchmarks/bench_minhash.py
+++ b/benchmarks/bench_minhash.py
@@ -66,7 +66,7 @@ class MinHashEncoder(BaseEstimator, TransformerMixin):
     ----------
     hash_dict_ : LRUDict
         Computed hashes.
-    
+
     Examples
     --------
     >>> enc = MinHashEncoder(n_components=5)
@@ -343,8 +343,9 @@ class MinHashEncoder(BaseEstimator, TransformerMixin):
                     unique_x[idx_slice],
                     hash_func,
                 )
-                for idx_slice in gen_even_slices(len(unique_x), 
-                n_jobs * self.batch_per_job)
+                for idx_slice in gen_even_slices(
+                    len(unique_x), n_jobs * self.batch_per_job
+                )
             )
             # Match the hashes of the unique value to the original values
             X_out = np.concatenate(unique_x_trans)[indices_x].reshape(
@@ -395,8 +396,9 @@ def benchmark(
     batch_per_job: int,
 ) -> None:
     X = data[dataset_size]
-    MinHashEncoder(batch=batched, n_jobs=n_jobs, 
-    batch_per_job=batch_per_job).fit(X).transform(X)
+    MinHashEncoder(batch=batched, n_jobs=n_jobs, batch_per_job=batch_per_job).fit(
+        X
+    ).transform(X)
 
 
 def plot(res: pd.DataFrame):
@@ -408,17 +410,25 @@ def plot(res: pd.DataFrame):
         memories = eval(str(ser["memory"]))
         _, _, kwargs = parse_func_repr(ser["call"])
         for time, memory in zip(times, memories):
-            rows.append((kwargs["batched"], kwargs["batch_per_job"], 
-            kwargs["n_jobs"], time, memory))
+            rows.append(
+                (
+                    kwargs["batched"],
+                    kwargs["batch_per_job"],
+                    kwargs["n_jobs"],
+                    time,
+                    memory,
+                )
+            )
 
-    df = pd.DataFrame(rows, columns=["batched", "batch_per_job", 
-    "n_jobs", "time", "memory"])
+    df = pd.DataFrame(
+        rows, columns=["batched", "batch_per_job", "n_jobs", "time", "memory"]
+    )
 
     # Create a new columns merging batched and batch_per_job
     # If batch is False, ignore batch_per_job
     df["config"] = df.apply(
         lambda row: f"batched={row['batched']}, batch_per_job={row['batch_per_job']}"
-        if row["batched"] == 'True'
+        if row["batched"] == "True"
         else "batched=False",
         axis=1,
     )
diff --git a/benchmarks/get_results.py b/benchmarks/get_results.py
index 5406673..94a6cfe 100644
--- a/benchmarks/get_results.py
+++ b/benchmarks/get_results.py
@@ -12,16 +12,16 @@ from pathlib import Path
 
 here = Path(__file__).parent
 
-expected_end = '_results.csv'
+expected_end = "_results.csv"
 
 param_name = "cardinality_threshold"
 for file in here.iterdir():
     if file.name.endswith(expected_end):
         df: pd.DataFrame = pd.read_csv(file)
-        name = file.name[:-len(expected_end)]
+        name = file.name[: -len(expected_end)]
 
-        card_unq = np.unique(df['param_sv__cardinality_threshold'])
-        comp_unq = np.unique(df['param_sv__high_card_str_transformer__n_components'])
+        card_unq = np.unique(df["param_sv__cardinality_threshold"])
+        comp_unq = np.unique(df["param_sv__high_card_str_transformer__n_components"])
 
         card_amnt = len(card_unq)
         comp_amnt = len(comp_unq)
@@ -35,18 +35,18 @@ for file in here.iterdir():
         ax.set_xticks(np.arange(comp_amnt))
         ax.set_yticks(np.arange(card_amnt))
         # ... and label them with the respective list entries
-        ax.set_xticklabels([f'n_components={v}' for v in comp_unq])
-        ax.set_yticklabels([f'cardinality={v}' for v in card_unq])
+        ax.set_xticklabels([f"n_components={v}" for v in comp_unq])
+        ax.set_yticklabels([f"cardinality={v}" for v in card_unq])
 
         # Rotate the tick labels and set their alignment.
-        plt.setp(ax.get_xticklabels(), rotation=45, ha="right",
-                 rotation_mode="anchor")
+        plt.setp(ax.get_xticklabels(), rotation=45, ha="right", rotation_mode="anchor")
 
         # Loop over data dimensions and create text annotations.
         for i in range(card_amnt):
             for j in range(comp_amnt):
-                text = ax.text(j, i, round(scores[i, j], 3),
-                               ha="center", va="center", color="w")
+                text = ax.text(
+                    j, i, round(scores[i, j], 3), ha="center", va="center", color="w"
+                )
 
         ax.set_title(name)
         fig.tight_layout()
diff --git a/benchmarks/similarity_scores_time_benchmark.py b/benchmarks/similarity_scores_time_benchmark.py
index dc782cc..3010e16 100644
--- a/benchmarks/similarity_scores_time_benchmark.py
+++ b/benchmarks/similarity_scores_time_benchmark.py
@@ -12,7 +12,8 @@ Parameters that are modified:
 
 # We filter out the warning asking us to specify the solver in the logistic_regression
 import warnings
-warnings.simplefilter(action='ignore', category=FutureWarning)
+
+warnings.simplefilter(action="ignore", category=FutureWarning)
 
 from time import time
 
@@ -30,10 +31,17 @@ from dirty_cat.datasets import fetch_traffic_violations
 data = fetch_traffic_violations()
 
 col_to_ohe = [
-    "alcohol", "arrest_type", "belts",
-    "commercial_license", "commercial_vehicle",
-    "fatal", "gender", "hazmat",
-    "property_damage", "race", "work_zone",
+    "alcohol",
+    "arrest_type",
+    "belts",
+    "commercial_license",
+    "commercial_vehicle",
+    "fatal",
+    "gender",
+    "hazmat",
+    "property_damage",
+    "race",
+    "work_zone",
 ]
 
 col_to_pass = ["year"]
@@ -51,25 +59,43 @@ X.reset_index(inplace=True, drop=True)
 y.reset_index(inplace=True, drop=True)
 
 transformers = [
-    ('one_hot', OneHotEncoder(sparse=False, handle_unknown='ignore'), col_to_ohe),
+    ("one_hot", OneHotEncoder(sparse=False, handle_unknown="ignore"), col_to_ohe),
     ("pass", "passthrough", col_to_pass),
 ]
 
 
-def benchmark(X_b, y_b, strat='k-means', limit=50000, n_proto=100, hash_dim=None, ngram_range=(3, 3)):
+def benchmark(
+    X_b,
+    y_b,
+    strat="k-means",
+    limit=50000,
+    n_proto=100,
+    hash_dim=None,
+    ngram_range=(3, 3),
+):
     X_b = X_b[:limit].copy()
     y_b = y_b[:limit].copy()
 
-    if strat == 'k-means':
-        sim_enc = SimilarityEncoder(ngram_range=ngram_range, categories='k-means',
-                                    hashing_dim=hash_dim, n_prototypes=n_proto, random_state=3498)
+    if strat == "k-means":
+        sim_enc = SimilarityEncoder(
+            ngram_range=ngram_range,
+            categories="k-means",
+            hashing_dim=hash_dim,
+            n_prototypes=n_proto,
+            random_state=3498,
+        )
     else:
-        sim_enc = SimilarityEncoder(ngram_range=ngram_range, categories='most_frequent',
-                                    hashing_dim=hash_dim, n_prototypes=n_proto, random_state=3498)
+        sim_enc = SimilarityEncoder(
+            ngram_range=ngram_range,
+            categories="most_frequent",
+            hashing_dim=hash_dim,
+            n_prototypes=n_proto,
+            random_state=3498,
+        )
 
     column_trans = ColumnTransformer(
-        transformers=transformers + [('sim_enc', sim_enc, ['description'])],
-        remainder='drop'
+        transformers=transformers + [("sim_enc", sim_enc, ["description"])],
+        remainder="drop",
     )
 
     t0 = time()
@@ -86,29 +112,33 @@ def benchmark(X_b, y_b, strat='k-means', limit=50000, n_proto=100, hash_dim=None
     return t_score_1, m_score, t_score_2
 
 
-def plot(bench, title=''):
-    sns.set(style='ticks', palette='muted')
-    hash_dims = ['Count', '2 ** 14', '2 ** 16', '2 ** 18', '2 ** 20']
+def plot(bench, title=""):
+    sns.set(style="ticks", palette="muted")
+    hash_dims = ["Count", "2 ** 14", "2 ** 16", "2 ** 18", "2 ** 20"]
     scores = []
     vectorizer = []
     strategy = []
 
     for i, e in enumerate(bench):
         vectorizer.extend([hash_dims[i % 5]] * (2 * len(e[0][1])))
-        strategy.extend(['k-means'] * len(e[0][1]))
-        strategy.extend(['most-frequent'] * len(e[1][1]))
+        strategy.extend(["k-means"] * len(e[0][1]))
+        strategy.extend(["most-frequent"] * len(e[1][1]))
         scores.extend(e[0][1])
         scores.extend(e[1][1])
 
-    df = pd.DataFrame(columns=['vectorizer', 'strategy', 'score'])
-    df['vectorizer'] = vectorizer
-    df['strategy'] = strategy
-    df['score'] = scores
+    df = pd.DataFrame(columns=["vectorizer", "strategy", "score"])
+    df["vectorizer"] = vectorizer
+    df["strategy"] = strategy
+    df["score"] = scores
 
     first = plt.figure()
-    ax = sns.boxplot(x='vectorizer', y='score', hue='strategy', data=df)
-    ax.set(title=title, xlabel='Vectorizer used', ylabel='Mean score on 10 cross validations')
-    ax.legend(loc='center left', bbox_to_anchor=(1, 0.5))
+    ax = sns.boxplot(x="vectorizer", y="score", hue="strategy", data=df)
+    ax.set(
+        title=title,
+        xlabel="Vectorizer used",
+        ylabel="Mean score on 10 cross validations",
+    )
+    ax.legend(loc="center left", bbox_to_anchor=(1, 0.5))
     first.tight_layout()
 
     vectorizer.clear()
@@ -118,41 +148,65 @@ def plot(bench, title=''):
 
     for i, e in enumerate(bench):
         vectorizer.extend([hash_dims[i % 5]] * 4)
-        strategy.extend(['K-means vect', 'K-means X-val', 'MF vect', 'MF X-val'])
+        strategy.extend(["K-means vect", "K-means X-val", "MF vect", "MF X-val"])
         times.extend([e[0][0], e[0][2] / 20, e[1][0], e[1][2] / 20])
 
-    df = pd.DataFrame(columns=['vectorizer', 'strategy/operation', 'time'])
-    df['vectorizer'] = vectorizer
-    df['strategy/operation'] = strategy
-    df['time'] = times
+    df = pd.DataFrame(columns=["vectorizer", "strategy/operation", "time"])
+    df["vectorizer"] = vectorizer
+    df["strategy/operation"] = strategy
+    df["time"] = times
 
     second = plt.figure()
-    ax1 = sns.barplot(x='vectorizer', y='time', hue='strategy/operation', data=df)
-    ax1.set(title=title, xlabel='Vectorizer used', ylabel='Time in seconds')
-    ax1.legend(loc='center left', bbox_to_anchor=(1, 0.5))
+    ax1 = sns.barplot(x="vectorizer", y="time", hue="strategy/operation", data=df)
+    ax1.set(title=title, xlabel="Vectorizer used", ylabel="Time in seconds")
+    ax1.legend(loc="center left", bbox_to_anchor=(1, 0.5))
     second.tight_layout()
 
-    title = title.replace(' ', '_').replace(':', '-').replace(',', '_').lower()
-    first.savefig(title + '_score.png')
-    second.savefig(title + '_time.png')
+    title = title.replace(" ", "_").replace(":", "-").replace(",", "_").lower()
+    first.savefig(title + "_score.png")
+    second.savefig(title + "_time.png")
     # first.show()
     # second.show(t)
 
 
 def loop(proto):
-    limits = sorted([X['description'].nunique(), 10000, 20000, 50000, 100000])
-    hash_dims = [None, 2 ** 14, 2 ** 16, 2 ** 18, 2 ** 20]
+    limits = sorted([X["description"].nunique(), 10000, 20000, 50000, 100000])
+    hash_dims = [None, 2**14, 2**16, 2**18, 2**20]
     bench = list()
     ngram = [(3, 3), (2, 4)]
     for limit in limits:
         for r in ngram:
             for h in hash_dims:
-                bench.append((benchmark(X, y, strat='k-means', limit=limit, n_proto=proto, hash_dim=h, ngram_range=r),
-                              benchmark(X, y, strat='most-frequent', limit=limit, n_proto=proto, hash_dim=h, ngram_range=r)))
-            title = 'N-gram range: %s, Rows: %d, Prototypes: %d, 20 CV' % (r.__str__(), limit, proto)
+                bench.append(
+                    (
+                        benchmark(
+                            X,
+                            y,
+                            strat="k-means",
+                            limit=limit,
+                            n_proto=proto,
+                            hash_dim=h,
+                            ngram_range=r,
+                        ),
+                        benchmark(
+                            X,
+                            y,
+                            strat="most-frequent",
+                            limit=limit,
+                            n_proto=proto,
+                            hash_dim=h,
+                            ngram_range=r,
+                        ),
+                    )
+                )
+            title = "N-gram range: %s, Rows: %d, Prototypes: %d, 20 CV" % (
+                r.__str__(),
+                limit,
+                proto,
+            )
             plot(bench, title)
             bench.clear()
 
 
-if __name__ == '__main__':
-    loop(100)
\ No newline at end of file
+if __name__ == "__main__":
+    loop(100)
diff --git a/benchmarks/supervectorizer_tuning.py b/benchmarks/supervectorizer_tuning.py
index f726c0f..cd1031f 100644
--- a/benchmarks/supervectorizer_tuning.py
+++ b/benchmarks/supervectorizer_tuning.py
@@ -11,9 +11,15 @@ from sklearn.model_selection import GridSearchCV
 from sklearn.pipeline import Pipeline
 
 from dirty_cat import SuperVectorizer
-from dirty_cat.datasets import fetch_open_payments, fetch_drug_directory, \
-    fetch_road_safety, fetch_midwest_survey, fetch_medical_charge, \
-    fetch_employee_salaries, fetch_traffic_violations
+from dirty_cat.datasets import (
+    fetch_open_payments,
+    fetch_drug_directory,
+    fetch_road_safety,
+    fetch_midwest_survey,
+    fetch_medical_charge,
+    fetch_employee_salaries,
+    fetch_traffic_violations,
+)
 
 from pathlib import Path
 from functools import wraps
@@ -23,25 +29,25 @@ from typing import List, Tuple
 
 def get_classification_datasets() -> List[Tuple[dict, str]]:
     return [
-        (fetch_open_payments(), 'open_payments'),
+        (fetch_open_payments(), "open_payments"),
         # (fetch_drug_directory(), 'drug_directory),
-        (fetch_road_safety(), 'road_safety'),
-        (fetch_midwest_survey(), 'midwest_survey'),
-        (fetch_traffic_violations(), 'traffic_violations'),
+        (fetch_road_safety(), "road_safety"),
+        (fetch_midwest_survey(), "midwest_survey"),
+        (fetch_traffic_violations(), "traffic_violations"),
     ]
 
 
 def get_regression_datasets() -> List[Tuple[dict, str]]:
     return [
-        (fetch_medical_charge(), 'medical_charge'),
-        (fetch_employee_salaries(), 'employee_salaries'),
+        (fetch_medical_charge(), "medical_charge"),
+        (fetch_employee_salaries(), "employee_salaries"),
     ]
 
 
 def get_dataset(info) -> Tuple[pd.DataFrame, pd.Series]:
-    df = pd.read_csv(info['path'], **info['read_csv_kwargs'])
-    y = df[info['y']]
-    X = df.drop(info['y'], axis=1).astype(str)
+    df = pd.read_csv(info["path"], **info["read_csv_kwargs"])
+    y = df[info["y"]]
+    X = df.drop(info["y"], axis=1).astype(str)
     return X, y
 
 
@@ -53,12 +59,12 @@ def set_logging(func):
         logger = logging.getLogger()
         logger.setLevel(logging_level)
 
-        formatter = logging.Formatter('%(asctime)s - [%(levelname)s] %(message)s')
-        formatter.datefmt = '%m/%d/%Y %H:%M:%S'
+        formatter = logging.Formatter("%(asctime)s - [%(levelname)s] %(message)s")
+        formatter.datefmt = "%m/%d/%Y %H:%M:%S"
 
-        path = Path(__file__).parent / f'tuning_{str(datetime.now())[:10]}.log'
+        path = Path(__file__).parent / f"tuning_{str(datetime.now())[:10]}.log"
 
-        fh = logging.FileHandler(filename=path, mode='w')
+        fh = logging.FileHandler(filename=path, mode="w")
         fh.setLevel(logging_level)
         fh.setFormatter(formatter)
 
@@ -70,28 +76,35 @@ def set_logging(func):
         # logger.addHandler(sh)
 
         return func(*args, **kwargs)
+
     return wrapper
 
 
 @set_logging
 def main():
-    logging.info('Launching !')
+    logging.info("Launching !")
 
     card_possibilities = [20, 30, 40, 50]
     n_comp_possibilities = [10, 30, 50]
 
-    logging.debug('Creating pipelines')
-    regression_pipeline = Pipeline([
-        ('sv', SuperVectorizer()),
-        ('estimator', RandomForestRegressor()),
-    ])
-    classification_pipeline = Pipeline([
-        ('sv', SuperVectorizer()),
-        ('estimator', RandomForestClassifier()),
-    ])
-
-    logging.debug(f'With cardinality possibilities: {card_possibilities} '
-                  f'and n_components possibilities: {n_comp_possibilities}')
+    logging.debug("Creating pipelines")
+    regression_pipeline = Pipeline(
+        [
+            ("sv", SuperVectorizer()),
+            ("estimator", RandomForestRegressor()),
+        ]
+    )
+    classification_pipeline = Pipeline(
+        [
+            ("sv", SuperVectorizer()),
+            ("estimator", RandomForestClassifier()),
+        ]
+    )
+
+    logging.debug(
+        f"With cardinality possibilities: {card_possibilities} "
+        f"and n_components possibilities: {n_comp_possibilities}"
+    )
     for pipeline, datasets in zip(
         [
             regression_pipeline,
@@ -100,25 +113,25 @@ def main():
         [
             get_regression_datasets(),
             get_classification_datasets(),
-        ]
+        ],
     ):
         for info, name in datasets:
             X, y = get_dataset(info)
-            if name != 'traffic_violations':
+            if name != "traffic_violations":
                 continue
 
-            csv_path = Path('.').resolve() / f'{name}_results.csv'
+            csv_path = Path(".").resolve() / f"{name}_results.csv"
             if csv_path.exists():
                 # If the results already exist, we'll skip to the next
-                logging.debug(f'Skipping {name} as {csv_path!s} was found')
+                logging.debug(f"Skipping {name} as {csv_path!s} was found")
                 continue
 
-            logging.debug(f'Running search on {name}')
+            logging.debug(f"Running search on {name}")
             grid = GridSearchCV(
                 estimator=pipeline,
                 param_grid={
-                    'sv__cardinality_threshold': card_possibilities,
-                    'sv__high_card_str_transformer__n_components': n_comp_possibilities,
+                    "sv__cardinality_threshold": card_possibilities,
+                    "sv__high_card_str_transformer__n_components": n_comp_possibilities,
                 },
                 n_jobs=30,
             )
@@ -126,8 +139,8 @@ def main():
 
             df = pd.DataFrame(grid.cv_results_)
             df.to_csv(csv_path)
-            logging.info(f'Saved search results in {csv_path!s}')
+            logging.info(f"Saved search results in {csv_path!s}")
 
 
-if __name__ == '__main__':
+if __name__ == "__main__":
     main()
diff --git a/dirty_cat/VERSION.txt b/dirty_cat/VERSION.txt
index ca91a08..88b0e14 100644
--- a/dirty_cat/VERSION.txt
+++ b/dirty_cat/VERSION.txt
@@ -1,1 +1,1 @@
-0.4.dev0
+0.4.0b1
diff --git a/dirty_cat/_feature_augmenter.py b/dirty_cat/_feature_augmenter.py
index 7dcf36d..ec5afef 100644
--- a/dirty_cat/_feature_augmenter.py
+++ b/dirty_cat/_feature_augmenter.py
@@ -32,7 +32,7 @@ class FeatureAugmenter(BaseEstimator, TransformerMixin):
         specyfying the transformer objects to be applied.
         table: str
             Name of the table to be joined.
-        column name: str,
+        column: str
             Name of table column to join on.
     main_key : str
         The key column name in the main table on which
diff --git a/dirty_cat/_gap_encoder.py b/dirty_cat/_gap_encoder.py
index a0dc6d4..014d6bd 100644
--- a/dirty_cat/_gap_encoder.py
+++ b/dirty_cat/_gap_encoder.py
@@ -30,6 +30,7 @@ from sklearn.neighbors import NearestNeighbors
 from sklearn.utils import check_random_state, gen_batches
 from sklearn.utils.extmath import row_norms, safe_sparse_dot
 from sklearn.utils.fixes import _object_dtype_isnan
+from sklearn.utils.validation import check_is_fitted
 
 from ._utils import check_input, parse_version
 
@@ -512,6 +513,7 @@ class GapEncoderColumn(BaseEstimator, TransformerMixin):
         H : 2-d array, shape (n_samples, n_topics)
             Transformed input.
         """
+        check_is_fitted(self, "H_dict_")
         # Check if first item has str or np.str_ type
         assert isinstance(X[0], str), "Input data is not string. "
         unq_X = np.unique(X)
@@ -775,6 +777,11 @@ class GapEncoder(BaseEstimator, TransformerMixin):
             Fitted :class:`~dirty_cat.GapEncoder` instance (self).
         """
 
+        # Check that n_samples >= n_components
+        if len(X) < self.n_components:
+            raise ValueError(
+                f"n_samples={len(X)} should be >= n_components={self.n_components}. "
+            )
         # Copy parameter rho
         self.rho_ = self.rho
         # If X is a dataframe, store its column names
@@ -810,7 +817,7 @@ class GapEncoder(BaseEstimator, TransformerMixin):
         H : 2-d array, shape (n_samples, n_topics * n_features)
             Transformed input.
         """
-
+        check_is_fitted(self, "fitted_models_")
         # Check input data shape
         X = check_input(X)
         X = self._handle_missing(X)
@@ -873,7 +880,7 @@ class GapEncoder(BaseEstimator, TransformerMixin):
 
                 - if the input data was a dataframe, its column names are used,
                 - otherwise, 'col1', ..., 'colN' are used as prefixes.
-                
+
             Prefixes can be manually set by passing a list for col_names.
         n_labels : int, default=3
             The number of labels used to describe each topic.
diff --git a/dirty_cat/_minhash_encoder.py b/dirty_cat/_minhash_encoder.py
index f45d4b8..4a56562 100644
--- a/dirty_cat/_minhash_encoder.py
+++ b/dirty_cat/_minhash_encoder.py
@@ -22,6 +22,7 @@ import numpy as np
 from joblib import Parallel, delayed, effective_n_jobs
 from sklearn.base import BaseEstimator, TransformerMixin
 from sklearn.utils import gen_even_slices, murmurhash3_32
+from sklearn.utils.validation import check_is_fitted
 
 from ._fast_hash import ngram_min_hash
 from ._string_distances import get_unique_ngrams
@@ -261,6 +262,7 @@ class MinHashEncoder(BaseEstimator, TransformerMixin):
         :obj:`~numpy.ndarray` of shape (n_samples, n_components)
             Transformed input.
         """
+        check_is_fitted(self, "hash_dict_")
         X = check_input(X)
         if self.minmax_hash:
             if self.n_components % 2 != 0:
diff --git a/dirty_cat/_similarity_encoder.py b/dirty_cat/_similarity_encoder.py
index 6367981..83c6511 100644
--- a/dirty_cat/_similarity_encoder.py
+++ b/dirty_cat/_similarity_encoder.py
@@ -27,6 +27,7 @@ from sklearn.neighbors import NearestNeighbors
 from sklearn.preprocessing import OneHotEncoder
 from sklearn.utils import check_random_state
 from sklearn.utils.fixes import _object_dtype_isnan
+from sklearn.utils.validation import check_is_fitted
 
 from ._string_distances import get_ngram_count, preprocess
 from ._utils import parse_version
@@ -219,7 +220,7 @@ class SimilarityEncoder(OneHotEncoder):
            categorical variable
         - 'k-means' : Computes the K nearest neighbors of K-mean centroids
            in order to choose the prototype categories
-           
+
         The categories used can be found in the ``categories_`` attribute.
     dtype : number type, default np.float64
         Desired dtype of output.
@@ -521,7 +522,7 @@ class SimilarityEncoder(OneHotEncoder):
         X_new : 2-d array, shape [n_samples, n_features_new]
             Transformed input.
         """
-
+        check_is_fitted(self, "categories_")
         if hasattr(X, "iloc") and X.isna().values.any():
             if self.handle_missing == "error":
                 raise ValueError(
diff --git a/dirty_cat/_super_vectorizer.py b/dirty_cat/_super_vectorizer.py
index 97d7d95..107e1e7 100644
--- a/dirty_cat/_super_vectorizer.py
+++ b/dirty_cat/_super_vectorizer.py
@@ -15,6 +15,7 @@ from sklearn import __version__ as sklearn_version
 from sklearn.base import TransformerMixin, clone
 from sklearn.compose import ColumnTransformer
 from sklearn.preprocessing import OneHotEncoder
+from sklearn.utils.validation import check_is_fitted
 
 from dirty_cat import DatetimeEncoder, GapEncoder
 from dirty_cat._utils import parse_version
@@ -607,6 +608,7 @@ class SuperVectorizer(ColumnTransformer):
             any result is a sparse matrix, everything will be converted to
             sparse matrices.
         """
+        check_is_fitted(self, attributes=["columns_"])
         if X.shape[1] != len(self.columns_):
             raise ValueError(
                 "Passed array does not match column count of "
diff --git a/dirty_cat/datasets/_fetching.py b/dirty_cat/datasets/_fetching.py
index b0eeb77..eeba1a8 100644
--- a/dirty_cat/datasets/_fetching.py
+++ b/dirty_cat/datasets/_fetching.py
@@ -818,5 +818,3 @@ def fetch_world_bank_indicator(
         load_dataframe=load_dataframe,
         data_directory=directory,
     )
-
-
diff --git a/examples/05_fuzzy_joining_and_FeatureAugmenter.py b/examples/05_fuzzy_joining_and_FeatureAugmenter.py
index b222e28..f30a759 100644
--- a/examples/05_fuzzy_joining_and_FeatureAugmenter.py
+++ b/examples/05_fuzzy_joining_and_FeatureAugmenter.py
@@ -374,7 +374,7 @@ print(f"Mean R2 score is {cv_r2_t.mean():.2f} +- {cv_r2_t.std():.2f}")
 
 #######################################################################
 # Instantiating the transformer
-# ............................
+# .............................
 
 y = df["Happiness score"]
 #######################################################################
@@ -394,7 +394,7 @@ fa = FeatureAugmenter(
 
 #################################################################
 # Fitting and transforming into the final table
-# ............................................
+# .............................................
 # To get our final joined table we will fit and transform the main table (df)
 # with our create instance of the |fa|:
 df_final = fa.fit_transform(df)
diff --git a/examples/06_deduplication.py b/examples/06_deduplication.py
index 0c3fcf8..20e0721 100644
--- a/examples/06_deduplication.py
+++ b/examples/06_deduplication.py
@@ -8,8 +8,7 @@ break many data-analyses steps that require exact matching, such as a
 'GROUP BY'.
 
 Merging the multiple variants of the same category or entity is known as
-*deduplication*. It is performed by the :func:`~dirty_cat.deduplicate`
-function.
+*deduplication*. It is performed by the |dd| function.
 
 Deduplication relies on *unsupervised learning*, to find structure in
 data without providing explicit labels/categories of the data a-priori.
@@ -17,6 +16,9 @@ Specifically clustering of the distance between strings can be used to
 find clusters of strings that are similar to each other (e.g. differ only
 by a misspelling) and hence gives us an easy tool to flag potentially
 misspelled category names in an unsupervised manner.
+
+
+.. |dd| replace:: :func:`~dirty_cat.deduplicate`
 """
 
 ###############################################################################
@@ -45,13 +47,16 @@ entries_per_medications = [500, 100, 1500]
 prob_mistake_per_letter = 0.05
 
 duplicated_names = make_deduplication_data(
-    medications, entries_per_medications, prob_mistake_per_letter,
-    random_state=42, # set seed for reproducibility
+    medications,
+    entries_per_medications,
+    prob_mistake_per_letter,
+    random_state=42,  # set seed for reproducibility
 )
 # we extract the unique medication names in the data & how often they appear
 unique_examples, counts = np.unique(duplicated_names, return_counts=True)
 # and build a series out of them
 import pandas as pd
+
 ex_series = pd.Series(counts, index=unique_examples)
 
 # This is our data:
@@ -100,13 +105,13 @@ sns.heatmap(
 
 ###############################################################################
 # Deduplication: suggest corrections of misspelled names
-# -----------------------------------------------------
+# ------------------------------------------------------
 #
-# The :func:`~dirty_cat.deduplicate` function uses clustering based on
+# The |dd| function uses clustering based on
 # string similarities to group duplicated names
 #
 # The number of clusters will need some adjustment depending on the data you have.
-# If no fixed number of clusters is given, `deduplicate` tries to set it automatically
+# If no fixed number of clusters is given, |dd| tries to set it automatically
 # via the `silhouette score <https://scikit-learn.org/stable/modules/clustering.html#silhouette-coefficient>`_.
 
 from dirty_cat import deduplicate
@@ -131,7 +136,7 @@ plt.ylabel("Counts")
 #
 # However, often the translation/deduplication won't be perfect and will require some tweaks.
 # In this case, we can construct and update a translation table based on the data
-# returned by `deduplicate`.
+# returned by |dd|.
 # It consists of the (potentially) misspelled category names as indices and the
 # (potentially) correct categories as values.
 
diff --git a/examples/README.txt b/examples/README.txt
index 8d0935a..822da6f 100644
--- a/examples/README.txt
+++ b/examples/README.txt
@@ -1,2 +1,2 @@
 Using dirty_cat
-===============
\ No newline at end of file
+===============
diff --git a/setup.cfg b/setup.cfg
index 88b6413..2ec8020 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -23,6 +23,7 @@ project_urls =
     Source = https://github.com/dirty-cat/dirty_cat
 
 [options]
+include_package_data = True
 packages = find:
 install_requires =
     scikit-learn>=0.23.0


Test patch:
diff --git a/dirty_cat/tests/test_datetime_encoder.py b/dirty_cat/tests/test_datetime_encoder.py
index 22705ea..124ae9d 100644
--- a/dirty_cat/tests/test_datetime_encoder.py
+++ b/dirty_cat/tests/test_datetime_encoder.py
@@ -1,5 +1,7 @@
+import pytest
 import numpy as np
 import pandas as pd
+from sklearn.exceptions import NotFittedError
 
 from dirty_cat._datetime_encoder import DatetimeEncoder
 
@@ -340,3 +342,14 @@ def test_transform():
     X = get_constant_date_array()
     enc = DatetimeEncoder(add_day_of_the_week=True)
     assert enc.fit_transform(X).shape[1] == 0
+
+def test_check_fitted_datetime_encoder():
+    """Test that calling transform before fit raises an error"""
+    X = get_datetime_array()[:, 0].reshape(-1, 1)
+    enc = DatetimeEncoder(add_day_of_the_week=True)
+    with pytest.raises(NotFittedError):
+        enc.transform(X)
+    
+    # Check that it works after fit
+    enc.fit(X)
+    enc.transform(X)
diff --git a/dirty_cat/tests/test_gap_encoder.py b/dirty_cat/tests/test_gap_encoder.py
index e9be30f..77c2c1f 100644
--- a/dirty_cat/tests/test_gap_encoder.py
+++ b/dirty_cat/tests/test_gap_encoder.py
@@ -2,6 +2,7 @@ import numpy as np
 import pandas as pd
 import pytest
 from sklearn import __version__ as sklearn_version
+from sklearn.exceptions import NotFittedError
 
 from dirty_cat import GapEncoder
 from dirty_cat._utils import parse_version
@@ -195,3 +196,23 @@ def test_missing_values(missing: str) -> None:
             r"'error' or 'zero_impute', got 'aaa'",
         ):
             enc.fit_transform(observations)
+
+
+def test_check_fitted_gap_encoder():
+    """Test that calling transform before fit raises an error"""
+    X = np.array([["alice"], ["bob"]])
+    enc = GapEncoder(n_components=2, random_state=42)
+    with pytest.raises(NotFittedError):
+        enc.transform(X)
+
+    # Check that it works after fit
+    enc.fit(X)
+    enc.transform(X)
+
+
+def test_small_sample():
+    """Test that having n_samples < n_components raises an error"""
+    X = np.array([["alice"], ["bob"]])
+    enc = GapEncoder(n_components=3, random_state=42)
+    with pytest.raises(ValueError, match="should be >= n_components"):
+        enc.fit_transform(X)
diff --git a/dirty_cat/tests/test_minhash_encoder.py b/dirty_cat/tests/test_minhash_encoder.py
index e88d380..0aa504f 100644
--- a/dirty_cat/tests/test_minhash_encoder.py
+++ b/dirty_cat/tests/test_minhash_encoder.py
@@ -6,6 +6,7 @@ import numpy as np
 import pandas as pd
 import pytest
 from sklearn.utils._testing import assert_array_equal, skip_if_no_parallel
+from sklearn.exceptions import NotFittedError
 
 from dirty_cat import MinHashEncoder
 
@@ -192,6 +193,7 @@ class DummyBackend(DEFAULT_JOBLIB_BACKEND):  # type: ignore
     The `count` attribute is used to check that the backend is used.
     Copied from https://github.com/scikit-learn/scikit-learn/blob/36958fb240fbe435673a9e3c52e769f01f36bec0/sklearn/ensemble/tests/test_forest.py  # noqa
     """
+
     def __init__(self, *args, **kwargs):
         self.count = 0
         super().__init__(*args, **kwargs)
@@ -216,7 +218,7 @@ def test_backend_respected():
 
     with joblib.parallel_backend("testing") as (ba, n_jobs):
         encoder.fit_transform(X)
-        
+
     assert ba.count > 0
 
 
@@ -242,3 +244,15 @@ def test_correct_arguments():
     with pytest.raises(ValueError, match=r"n_components should be even"):
         encoder = MinHashEncoder(n_components=3, minmax_hash=True)
         encoder.fit_transform(X)
+
+def test_check_fitted_minhash_encoder():
+    """Test that calling transform before fit raises an error"""
+    encoder = MinHashEncoder(n_components=3)
+    X = np.array(["a", "b", "c", "d", "e", "f", "g", "h"])[:, None]
+    with pytest.raises(NotFittedError):
+        encoder.transform(X)
+    
+    # Check that it works after fitting
+    encoder.fit(X)
+    encoder.transform(X)
+
diff --git a/dirty_cat/tests/test_similarity_encoder.py b/dirty_cat/tests/test_similarity_encoder.py
index 292c7c2..51bc929 100644
--- a/dirty_cat/tests/test_similarity_encoder.py
+++ b/dirty_cat/tests/test_similarity_encoder.py
@@ -4,6 +4,7 @@ import numpy as np
 import numpy.testing
 import pytest
 from sklearn import __version__ as sklearn_version
+from sklearn.exceptions import NotFittedError
 
 from dirty_cat import SimilarityEncoder
 from dirty_cat._similarity_encoder import get_kmeans_prototypes, ngram_similarity_matrix
@@ -385,3 +386,12 @@ def test_get_features() -> None:
         "x0_}",
         "x0_~",
     ]
+
+def test_check_fitted_super_vectorizer():
+    """Test that calling transform before fit raises an error"""
+    sim_enc = SimilarityEncoder()
+    X = np.array(["%s" % chr(i) for i in range(32, 127)]).reshape((-1, 1))
+    with pytest.raises(NotFittedError):
+        sim_enc.transform(X)
+    sim_enc.fit(X)
+    sim_enc.transform(X)
diff --git a/dirty_cat/tests/test_super_vectorizer.py b/dirty_cat/tests/test_super_vectorizer.py
index ce600c5..0806c0d 100644
--- a/dirty_cat/tests/test_super_vectorizer.py
+++ b/dirty_cat/tests/test_super_vectorizer.py
@@ -7,6 +7,7 @@ import sklearn
 from sklearn.exceptions import NotFittedError
 from sklearn.preprocessing import StandardScaler
 from sklearn.utils.validation import check_is_fitted
+from sklearn.exceptions import NotFittedError
 
 from dirty_cat import GapEncoder, SuperVectorizer
 from dirty_cat._utils import parse_version
@@ -445,3 +446,15 @@ def test_passthrough():
     dirty_flat_trans_df = X_enc_dirty.to_numpy().ravel().tolist()
     assert all(map(_is_equal, zip(dirty_flat_df, dirty_flat_trans_df)))
     assert (X_clean.to_numpy() == X_enc_clean.to_numpy()).all()
+
+def test_check_fitted_super_vectorizer():
+    """Test that calling transform before fit raises an error"""
+    X = _get_clean_dataframe()
+    sv = SuperVectorizer()
+    with pytest.raises(NotFittedError):
+        sv.transform(X)
+    
+    # Test that calling transform after fit works
+    sv.fit(X)
+    sv.transform(X)
+
diff --git a/dirty_cat/tests/test_target_encoder.py b/dirty_cat/tests/test_target_encoder.py
index e45b396..cf64192 100644
--- a/dirty_cat/tests/test_target_encoder.py
+++ b/dirty_cat/tests/test_target_encoder.py
@@ -1,5 +1,6 @@
 import numpy as np
 import pytest
+from sklearn.exceptions import NotFittedError
 
 from dirty_cat import _target_encoder
 
@@ -275,3 +276,15 @@ def test_errors():
         enc4.transform(X2)
     with pytest.raises(ValueError, match="Found unknown categories "):
         enc4.transform(X3)
+
+def test_check_fitted_target_encoder():
+    """Test that calling transform before fit raises an error"""
+    enc = _target_encoder.TargetEncoder()
+    X = [[1], [2], [2], [3], [4]]
+    y = np.array([1, 2, 3, 4, 5])
+
+    with pytest.raises(NotFittedError, match="This TargetEncoder instance is not"):
+        enc.transform(X)
+    
+    enc.fit(X, y)
+    enc.transform(X)