Repository: onnx/sklearn-onnx

Issue:
BooleanTensorType not supported?
I tried using `convert_sklearn()` with one of my `initial_types` set to `BooleanTensorType([None, 1])`: the conversion works but then when I try to load the model with onnxruntime, I get the following error:
```
onnxruntime.capi.onnxruntime_pybind11_state.InvalidGraph: [ONNXRuntimeError] : 10 : INVALID_GRAPH : This is an invalid model. Type Error: Type 'tensor(bool)' of input parameter (input1) of operator (ArrayFeatureExtractor) in node (ArrayFeatureExtractor) is invalid.
```

Is that expected?


Discussion:
What model are you trying to convert? Can you explain the use case where you need boolean as input?
@prabhat00155 I'm not sure exactly how to answer that, but here I try.

It's a model I'm building myself, it takes samples from a dataframe and some of the features are boolean values (yes or no values) and it predicts a number. You can think of it as for example a way to predict, say, prices from a set of product characteristics

It is possible I am missing something and I could convert the boolean input into another type, but my ML knowledge is not good enough to be sure what to use instead :)

For the record, my model is a pipeline with:
- a `ColumnTransformer` to encode some of the feature
  - I was planning to not encode it
- a XGBoost regressor (which supports boolean input)
I see, for the time being, you may cast those boolean features to int using dataframe.astype(int). Meanwhile, I'll see what needs to be done in order to support bool.
@prabhat00155 ok thanks. Actually, I had to cast them to float because I got the following error:
```
  File "/home/victor/.cache/pypoetry/virtualenvs/venv-51Be8MFl-py3.7/lib/python3.7/site-packages/skl2onnx/shape_calculators/concat.py", line 41, in calculate_sklearn_concat
    + inps)
RuntimeError: Columns must have the same type. C++ backends do not support mixed types. Inputs:
Variable(raw_name='variable', onnx_name='variable', type=FloatTensorType(shape=[None, 1]))
Variable(raw_name='variable', onnx_name='variable1', type=FloatTensorType(shape=[None, 1]))
Variable(raw_name='variable', onnx_name='variable2', type=FloatTensorType(shape=[None, 1]))
Variable(raw_name='variable', onnx_name='variable3', type=FloatTensorType(shape=[None, 1]))
Variable(raw_name='variable', onnx_name='variable4', type=FloatTensorType(shape=[None, 1]))
Variable(raw_name='feat1', onnx_name='has_treatment', type=Int64TensorType(shape=[None, 1]))
Variable(raw_name='feat2', onnx_name='is_beartex', type=Int64TensorType(shape=[None, 1]))
Variable(raw_name='feat3', onnx_name='is_norax', type=Int64TensorType(shape=[None, 1]))
Variable(raw_name='variable', onnx_name='variable5', type=FloatTensorType(shape=[None, 1]))
Variable(raw_name='variable', onnx_name='variable6', type=FloatTensorType(shape=[None, 1]))
Variable(raw_name='variable', onnx_name='variable7', type=FloatTensorType(shape=[None, 1]))
Variable(raw_name='variable', onnx_name='variable8', type=FloatTensorType(shape=[None, 1]))
Variable(raw_name='variable', onnx_name='variable9', type=FloatTensorType(shape=[None, 1]))
Variable(raw_name='variable', onnx_name='variable10', type=FloatTensorType(shape=[None, 1]))
Variable(raw_name='variable', onnx_name='variable11', type=FloatTensorType(shape=[None, 1]))
Variable(raw_name='variable', onnx_name='variable12', type=FloatTensorType(shape=[None, 1]))
Variable(raw_name='variable', onnx_name='variable13', type=FloatTensorType(shape=[None, 1]))
```

The other variables are the result of scalers and encoders. So I suppose that even If I can use boolean tensor variable, until the C++ backends support mixed type, it won't be practically usable, right?
Can you paste your code here? If you can't share the entire code, just share the part where you build the model pipeline and convert it to onnx.
@prabhat00155 here it is:
```py
train_df, test_df, train_labels, test_labels = train_test_split(
    full_df, full_labels, test_size=.2, random_state=None
)

col_transformer = _column_tranformer_fitted_from_df(full_df)

search = RandomizedSearchCV(
    XGBRegressor(verbose=0, objective='reg:squarederror'),
    param_distributions={
        "colsample_bytree": sp_uniform(),
        "gamma": sp_uniform(.1, 1),
        'learning_rate': sp_uniform(.1, .6),
        'max_depth': sp_randint(10, 30),
        'min_child_weight': sp_uniform(0, 3),
        'n_estimators': range(10, 75),  # nb de pas d'apprentissange pour chaque cas
    },
    cv=5, n_iter=100, n_jobs=multiprocessing.cpu_count()
)
search.fit(self._col_transformer.transform(train_df), train_labels)
regressor = XGBRegressor(verbose=0, objective='reg:squarederror', **search.best_params_)
regressor.fit(
    col_transformer.transform(train_df),
    train_labels
)
model = Pipeline(
    steps=[('preprocessor', col_transformer),
           ('regressor', regressor)]
)

update_registered_converter(
    XGBRegressor, 'XGBRegressor',
    calculate_linear_regressor_output_shapes,
    convert_xgboost
)
onnx = convert_sklearn(
    model,
    initial_types=_convert_dataframe_schema(self._for_eval_df)
)


def _column_tranformer_fitted_from_df(data: pd.DataFrame) -> ColumnTransformer:
    def transformer_for_column(column: pd.Series):
        if column.dtype in ['float64', 'float32']:
            return MinMaxScaler()
        if column.dtype in ['bool']:
            return 'passthrough'
        if column.dtype in ['O']:
            # TODO because of https://github.com/onnx/sklearn-onnx/issues/321
            return OrdinalEncoder()
        raise ValueError(f'Unexpected column dtype for {column.name}:{column.dtype}')

    return ColumnTransformer(
        [(col, transformer_for_column(data[col]), [col]) for col in data.columns],
        remainder='drop'
    ).fit(data)


def _convert_dataframe_schema(data: pd.DataFrame) -> List[Tuple[str, onnxtypes.DataType]]:
    def type_for_column(column: pd.Series):
        if column.dtype in ['float64', 'float32']:
            # onnx does not really support float64 (DoubleTensorType does not work with TreeEnsembleRegressor)
            return onnxtypes.FloatTensorType([None, 1])
        if column.dtype in ['bool']:
            return onnxtypes.BooleanTensorType([None, 1])
        if column.dtype in ['O']:
            return onnxtypes.StringTensorType([None, 1])
        raise ValueError(f'Unexpected column dtype for {column.name}:{column.dtype}')

    return [(col, type_for_column(data[col])) for col in data.columns]

```
Sorry for the late reply, was busy with another work item. I seem to be getting a different error with your code. Anyways, we need to add support for boolean type(will add this work item). Do you get this error:https://github.com/onnx/sklearn-onnx/issues/399#issuecomment-583499082, with the code you pasted above?
@prabhat00155 yes I do, I suppose we are limited by onnxruntime there?
Are you running this on `'https://raw.githubusercontent.com/amueller/scipy-2017-sklearn/091d371/notebooks/datasets/titanic3.csv'` dataset as well?
@prabhat00155 yes, you can also refer to https://gist.github.com/victornoel/c4da3089f6792ac1b6fdcdb583886f97: simply uncomment the line at the beginning to use the `female` boolean column instead of the `sex` one.
Made some changes to your code(mapped bool to float):
```
import os
import warnings
from typing import List, Tuple

import onnxruntime as rt
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.compose import ColumnTransformer
from sklearn.model_selection import RandomizedSearchCV
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import MinMaxScaler, OneHotEncoder
from scipy.stats import randint as sp_randint, uniform as sp_uniform
from xgboost import XGBRegressor
from onnxmltools.convert.xgboost.operator_converters.XGBoost import convert_xgboost
import onnxmltools.convert.common.data_types as onnxtypes
from skl2onnx import update_registered_converter, convert_sklearn
from skl2onnx.common.shape_calculator import calculate_linear_regressor_output_shapes


warnings.simplefilter(action='ignore', category=FutureWarning)
os.environ["PYTHONWARNINGS"] = 'ignore'


def test():
    data = pd.read_csv(('https://raw.githubusercontent.com/amueller/'
                        'scipy-2017-sklearn/091d371/notebooks/datasets/titanic3.csv'))
    data['female'] = data['sex'] == 'female'
    #data.drop('sex', axis=1, inplace=True)
    #data = data[['age', 'fare', 'female', 'embarked', 'pclass', 'survived']]
    data = data[['age', 'fare', 'female', 'embarked', 'pclass', 'survived']]

    for col in data:
        dtype = data[col].dtype
        if dtype in ['float64', 'float32']:
            data[col].fillna(0., inplace=True)
        if dtype in ['int64']:
            data[col].fillna(0, inplace=True)
        elif dtype in ['O']:
            data[col].fillna('N/A', inplace=True)

    full_df = data.drop('survived', axis=1)
    full_labels = data['survived']

    train_df, test_df, train_labels, test_labels = train_test_split(
        full_df, full_labels, test_size=.2
    )

    col_transformer = _column_tranformer_fitted_from_df(full_df)

    search = RandomizedSearchCV(
        XGBRegressor(verbose=0, objective='reg:squarederror'),
        param_distributions={
            "colsample_bytree": sp_uniform(),
            "gamma": sp_uniform(.1, 1),
            'learning_rate': sp_uniform(.1, .6),
            'max_depth': sp_randint(10, 30),
            'min_child_weight': sp_uniform(0, 3),
            'n_estimators': range(10, 75),
        },
        cv=5, n_iter=10, n_jobs=-1
    )
    search.fit(col_transformer.transform(train_df), train_labels)
    regressor = XGBRegressor(verbose=0, objective='reg:squarederror', **search.best_params_)
    regressor.fit(
        col_transformer.transform(train_df),
        train_labels
    )
    model = Pipeline(
        steps=[('preprocessor', col_transformer),
               ('regressor', regressor)]
    )

    update_registered_converter(
        XGBRegressor, 'XGBRegressor',
        calculate_linear_regressor_output_shapes,
        convert_xgboost
    )
    onnx = convert_sklearn(
        model,
        initial_types=_convert_dataframe_schema(full_df)
    )

    session = rt.InferenceSession(onnx.SerializeToString())

    pred_skl = model.predict(test_df)
    pred_onx = _predict(session, test_df)

    diff = np.sort(np.abs(np.squeeze(pred_skl) - np.squeeze(pred_onx)))[-5:]
    print(diff)
    print('min(Y)-max(Y):', min(test_labels), max(test_labels))


def _column_tranformer_fitted_from_df(data: pd.DataFrame) -> ColumnTransformer:
    def transformer_for_column(column: pd.Series):
        if column.dtype in ['float64', 'float32', 'int64']:
            return MinMaxScaler()
        if column.dtype in ['bool']:
            return 'passthrough'
        if column.dtype in ['O']:
            return OneHotEncoder(drop='first')
        raise ValueError(f'Unexpected column dtype for {column.name}:{column.dtype}')

    return ColumnTransformer(
        [(col, transformer_for_column(data[col]), [col]) for col in data.columns],
        remainder='drop'
    ).fit(data)


def _convert_dataframe_schema(data: pd.DataFrame) -> List[Tuple[str, onnxtypes.DataType]]:
    def type_for_column(column: pd.Series):
        if column.dtype in ['float64', 'float32', 'bool']:
            # onnx does not really support float64 (DoubleTensorType does not work with TreeEnsembleRegressor)
            return onnxtypes.FloatTensorType([None, 1])
        if column.dtype in ['int64']:
            return onnxtypes.Int64TensorType([None, 1])
        #if column.dtype in ['bool']:
        #    return onnxtypes.BooleanTensorType([None, 1])
        if column.dtype in ['O']:
            return onnxtypes.StringTensorType([None, 1])
        raise ValueError(f'Unexpected column dtype for {column.name}:{column.dtype}')

    return [(col, type_for_column(data[col])) for col in data.columns]


def _predict(session: rt.InferenceSession, data: pd.DataFrame) -> pd.Series:
    def _correctly_typed_column(column: pd.Series) -> pd.Series:
        if column.dtype in ['float64', 'bool']:
            return column.astype(np.float32)
        return column

    def _correctly_shaped_values(values):
        return values.reshape((values.shape[0], 1))

    inputs = {
        c: _correctly_shaped_values(_correctly_typed_column(data[c]).values)
        for c in data.columns
    }

    return pd.Series(
        session.run(None, inputs)[0].reshape(-1),
        index=data.index
    )
```
If I call test() now, I get the following output:
[1.1920929e-07 1.1920929e-07 1.1920929e-07 1.1920929e-07 1.4901161e-07]
min(Y)-max(Y): 0 1
@prabhat00155 yes, I already know that mapping the bool to float was a possibility.

I was wondering instead of I could be using boolean directly.
Yeah, as I mentioned before, we need to add support for bool in the converters. I'll mark this issue as enhancement and evaluate the work that needs to be done to support bool. In the mean time, you may use the casting approach. Thanks!
For the record, following the fix (#411), I tested again and there is now just one problem with concat:
```
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/victor/test/test2.py", line 81, in test
    initial_types=_convert_dataframe_schema(full_df)
  File "/home/victor/.cache/pypoetry/virtualenvs/test-onnx-Gs-mCkZB-py3.7/src/skl2onnx/skl2onnx/convert.py", line 134, in convert_sklearn
    topology.compile()
  File "/home/victor/.cache/pypoetry/virtualenvs/test-onnx-Gs-mCkZB-py3.7/src/skl2onnx/skl2onnx/common/_topology.py", line 892, in compile
    self._infer_all_types()
  File "/home/victor/.cache/pypoetry/virtualenvs/test-onnx-Gs-mCkZB-py3.7/src/skl2onnx/skl2onnx/common/_topology.py", line 745, in _infer_all_types
    operator.infer_types()
  File "/home/victor/.cache/pypoetry/virtualenvs/test-onnx-Gs-mCkZB-py3.7/src/skl2onnx/skl2onnx/common/_topology.py", line 208, in infer_types
    shape_calc(self)
  File "/home/victor/.cache/pypoetry/virtualenvs/test-onnx-Gs-mCkZB-py3.7/src/skl2onnx/skl2onnx/shape_calculators/concat.py", line 43, in calculate_sklearn_concat
    inps, outs))
RuntimeError: Columns must have the same type. C++ backends do not support mixed types.
Inputs:
Variable(raw_name='variable', onnx_name='variable', type=FloatTensorType(shape=[None, 1]))
Variable(raw_name='variable', onnx_name='variable1', type=FloatTensorType(shape=[None, 1]))
Variable(raw_name='female', onnx_name='female', type=BooleanTensorType(shape=[None, 1]))
Variable(raw_name='variable', onnx_name='variable2', type=FloatTensorType(shape=[None, 4]))
Variable(raw_name='variable', onnx_name='variable3', type=FloatTensorType(shape=[None, 1]))
Outputs:
Variable(raw_name='transformed_column', onnx_name='transformed_column', type=FloatTensorType(shape=[None, None]))
```

Resolving patch:
diff --git a/skl2onnx/operator_converters/concat_op.py b/skl2onnx/operator_converters/concat_op.py
--- a/skl2onnx/operator_converters/concat_op.py
+++ b/skl2onnx/operator_converters/concat_op.py
@@ -4,13 +4,25 @@
 # license information.
 # --------------------------------------------------------------------------
 
-from ..common._apply_operation import apply_concat
+from ..common._apply_operation import apply_concat, apply_cast
 from ..common._registration import register_converter
 
 
 def convert_sklearn_concat(scope, operator, container):
-    apply_concat(scope, [s for s in operator.input_full_names],
-                 operator.outputs[0].full_name, container, axis=1)
+    exptype = operator.outputs[0]
+    new_inputs = []
+    for inp in operator.inputs:
+        if inp.type == exptype:
+            new_inputs.append(inp.full_name)
+            continue
+        name = scope.get_unique_variable_name("{}_cast".format(inp.full_name))
+        res = exptype.type.to_onnx_type()
+        et = res.tensor_type.elem_type
+        apply_cast(scope, inp.full_name, name, container, to=et)
+        new_inputs.append(name)
+
+    apply_concat(scope, new_inputs, operator.outputs[0].full_name,
+                 container, axis=1)
 
 
 register_converter('SklearnConcat', convert_sklearn_concat)
diff --git a/skl2onnx/operator_converters/feature_selection.py b/skl2onnx/operator_converters/feature_selection.py
--- a/skl2onnx/operator_converters/feature_selection.py
+++ b/skl2onnx/operator_converters/feature_selection.py
@@ -5,9 +5,7 @@
 # --------------------------------------------------------------------------
 
 from ..proto import onnx_proto
-from ..common._apply_operation import apply_cast
 from ..common._registration import register_converter
-from ..common.data_types import FloatTensorType, FloatType
 
 
 def convert_sklearn_feature_selection(scope, operator, container):
@@ -19,13 +17,7 @@ def convert_sklearn_feature_selection(scope, operator, container):
             "Model '{}' did not select any feature. "
             "This model cannot be converted into ONNX."
             "".format(op.__class__.__name__))
-    needs_cast = not isinstance(operator.inputs[0].type,
-                                (FloatTensorType, FloatType))
-    if needs_cast:
-        output_name = scope.get_unique_variable_name('output')
-    else:
-        output_name = operator.outputs[0].full_name
-
+    output_name = operator.outputs[0].full_name
     if index.any():
         column_indices_name = scope.get_unique_variable_name('column_indices')
 
@@ -41,9 +33,6 @@ def convert_sklearn_feature_selection(scope, operator, container):
     else:
         container.add_node('ConstantOfShape', operator.inputs[0].full_name,
                            output_name, op_version=9)
-    if needs_cast:
-        apply_cast(scope, output_name, operator.outputs[0].full_name,
-                   container, to=onnx_proto.TensorProto.FLOAT)
 
 
 register_converter('SklearnGenericUnivariateSelect',
diff --git a/skl2onnx/shape_calculators/concat.py b/skl2onnx/shape_calculators/concat.py
--- a/skl2onnx/shape_calculators/concat.py
+++ b/skl2onnx/shape_calculators/concat.py
@@ -5,7 +5,11 @@
 # --------------------------------------------------------------------------
 
 from ..common._registration import register_shape_calculator
-from ..common.data_types import FloatType, Int64Type, StringType, TensorType
+from ..common.data_types import (
+    FloatType, Int64Type, StringType, TensorType,
+    BooleanTensorType, FloatTensorType,
+    Int64TensorType, StringTensorType,
+    DoubleTensorType)
 from ..common.utils import check_input_and_output_numbers
 
 
@@ -30,18 +34,47 @@ def calculate_sklearn_concat(operator):
         else:
             C = None
             break
-        nt = i.type.__class__.__name__
         if len(seen_types) == 0:
-            seen_types.append(nt)
-        elif nt != seen_types[0]:
-            inps = "\n".join(str(v) for v in operator.inputs)
-            outs = "\n".join(str(v) for v in operator.outputs)
+            seen_types.append(i.type)
+
+    def more_generic(t1, t2):
+        if isinstance(t1, TensorType):
+            if not isinstance(t2, TensorType):
+                raise RuntimeError(
+                    "Cannot merge columns with types {} and {}."
+                    "Inputs:\n{}\nOutputs:\n{}".format(
+                        t1, t2, operator.inputs, operator.outputs))
+            for ts in [StringTensorType, DoubleTensorType, FloatTensorType,
+                       Int64TensorType, BooleanTensorType]:
+                if isinstance(t1, ts) or isinstance(t2, ts):
+                    return ts
             raise RuntimeError(
-                "Columns must have the same type. "
-                "C++ backends do not support mixed types.\n"
+                "Cannot merge columns with types {} and {}."
+                "Inputs:\n{}\nOutputs:\n{}".format(
+                    t1, t2, operator.inputs, operator.outputs))
+        raise NotImplementedError(
+                "Columns must be tensors."
                 "Inputs:\n{}\nOutputs:\n{}".format(
-                    inps, outs))
-    operator.outputs[0].type.shape = [N, C]
+                    operator.inputs, operator.outputs))
+
+    # Let's determine the resulting type
+    final_type = None
+    for seen in seen_types:
+        if final_type is None:
+            final_type = seen
+        elif seen != final_type:
+            if more_generic(final_type, seen):
+                final_type = seen
+
+    if final_type is None:
+        raise NotImplementedError(
+            "Columns must be tensors."
+            "Inputs:\n{}\nOutputs:\n{}".format(
+                operator.inputs, operator.outputs))
+    if final_type != operator.outputs[0].type:
+        operator.outputs[0].type = type(final_type)([N, C])
+    else:
+        operator.outputs[0].type.shape = [N, C]
 
 
 register_shape_calculator('SklearnConcat', calculate_sklearn_concat)


Test patch:
diff --git a/tests/datasets/small_titanic.csv b/tests/datasets/small_titanic.csv
new file mode 100644
--- /dev/null
+++ b/tests/datasets/small_titanic.csv
@@ -0,0 +1,39 @@
+pclass,survived,name,sex,age,sibsp,parch,ticket,fare,cabin,embarked,boat,body,home.dest
+1,1,"Allen, Miss. Elisabeth Walton",female,29,0,0,24160,211.3375,B5,S,2,,"St Louis, MO"
+1,1,"Allison, Master. Hudson Trevor",male,0.9167,1,2,113781,151.5500,C22 C26,S,11,,"Montreal, PQ / Chesterville, ON"
+1,0,"Allison, Miss. Helen Loraine",female,2,1,2,113781,151.5500,C22 C26,S,,,"Montreal, PQ / Chesterville, ON"
+1,0,"Allison, Mr. Hudson Joshua Creighton",male,30,1,2,113781,151.5500,C22 C26,S,,135,"Montreal, PQ / Chesterville, ON"
+1,0,"Allison, Mrs. Hudson J C (Bessie Waldo Daniels)",female,25,1,2,113781,151.5500,C22 C26,S,,,"Montreal, PQ / Chesterville, ON"
+1,1,"Anderson, Mr. Harry",male,48,0,0,19952,26.5500,E12,S,3,,"New York, NY"
+1,1,"Andrews, Miss. Kornelia Theodosia",female,63,1,0,13502,77.9583,D7,S,10,,"Hudson, NY"
+1,0,"Andrews, Mr. Thomas Jr",male,39,0,0,112050,0.0000,A36,S,,,"Belfast, NI"
+1,1,"Appleton, Mrs. Edward Dale (Charlotte Lamson)",female,53,2,0,11769,51.4792,C101,S,D,,"Bayside, Queens, NY"
+1,0,"Artagaveytia, Mr. Ramon",male,71,0,0,PC 17609,49.5042,,C,,22,"Montevideo, Uruguay"
+1,0,"Astor, Col. John Jacob",male,47,1,0,PC 17757,227.5250,C62 C64,C,,124,"New York, NY"
+1,1,"Astor, Mrs. John Jacob (Madeleine Talmadge Force)",female,18,1,0,PC 17757,227.5250,C62 C64,C,4,,"New York, NY"
+1,1,"Aubart, Mme. Leontine Pauline",female,24,0,0,PC 17477,69.3000,B35,C,9,,"Paris, France"
+1,1,"Barber, Miss. Ellen ""Nellie""",female,26,0,0,19877,78.8500,,S,6,,
+1,1,"Barkworth, Mr. Algernon Henry Wilson",male,80,0,0,27042,30.0000,A23,S,B,,"Hessle, Yorks"
+1,0,"Baumann, Mr. John D",male,,0,0,PC 17318,25.9250,,S,,,"New York, NY"
+1,0,"Baxter, Mr. Quigg Edmond",male,24,0,1,PC 17558,247.5208,B58 B60,C,,,"Montreal, PQ"
+1,1,"Baxter, Mrs. James (Helene DeLaudeniere Chaput)",female,50,0,1,PC 17558,247.5208,B58 B60,C,6,,"Montreal, PQ"
+1,1,"Bazzani, Miss. Albina",female,32,0,0,11813,76.2917,D15,C,8,,
+1,0,"Beattie, Mr. Thomson",male,36,0,0,13050,75.2417,C6,C,A,,"Winnipeg, MN"
+1,1,"Beckwith, Mr. Richard Leonard",male,37,1,1,11751,52.5542,D35,S,5,,"New York, NY"
+1,1,"Beckwith, Mrs. Richard Leonard (Sallie Monypeny)",female,47,1,1,11751,52.5542,D35,S,5,,"New York, NY"
+1,1,"Behr, Mr. Karl Howell",male,26,0,0,111369,30.0000,C148,C,5,,"New York, NY"
+1,1,"Bidois, Miss. Rosalie",female,42,0,0,PC 17757,227.5250,,C,4,,
+1,1,"Bird, Miss. Ellen",female,29,0,0,PC 17483,221.7792,C97,S,8,,
+1,0,"Birnbaum, Mr. Jakob",male,25,0,0,13905,26.0000,,C,,148,"San Francisco, CA"
+1,1,"Bishop, Mr. Dickinson H",male,25,1,0,11967,91.0792,B49,C,7,,"Dowagiac, MI"
+1,1,"Bishop, Mrs. Dickinson H (Helen Walton)",female,19,1,0,11967,91.0792,B49,C,7,,"Dowagiac, MI"
+1,1,"Bissette, Miss. Amelia",female,35,0,0,PC 17760,135.6333,C99,S,8,,
+1,1,"Bjornstrom-Steffansson, Mr. Mauritz Hakan",male,28,0,0,110564,26.5500,C52,S,D,,"Stockholm, Sweden / Washington, DC"
+1,0,"Blackwell, Mr. Stephen Weart",male,45,0,0,113784,35.5000,T,S,,,"Trenton, NJ"
+1,1,"Blank, Mr. Henry",male,40,0,0,112277,31.0000,A31,C,7,,"Glen Ridge, NJ"
+1,1,"Bonnell, Miss. Caroline",female,30,0,0,36928,164.8667,C7,S,8,,"Youngstown, OH"
+1,1,"Bonnell, Miss. Elizabeth",female,58,0,0,113783,26.5500,C103,S,8,,"Birkdale, England Cleveland, Ohio"
+1,0,"Borebank, Mr. John James",male,42,0,0,110489,26.5500,D22,S,,,"London / Winnipeg, MB"
+1,1,"Bowen, Miss. Grace Scott",female,45,0,0,PC 17608,262.3750,,C,4,,"Cooperstown, NY"
+1,1,"Bowerman, Miss. Elsie Edith",female,22,0,1,113505,55.0000,E33,S,6,,"St Leonards-on-Sea, England Ohio"
+1,1,"Bradley, Mr. George (""George Arthur Brayton"")",male,,0,0,111427,26.5500,,S,9,,"Los Angeles, CA"
diff --git a/tests/test_sklearn_concat.py b/tests/test_sklearn_concat.py
new file mode 100644
--- /dev/null
+++ b/tests/test_sklearn_concat.py
@@ -0,0 +1,132 @@
+import os
+import unittest
+import onnxruntime as rt
+import pandas as pd
+import numpy as np
+from sklearn.tree import DecisionTreeRegressor
+from sklearn.model_selection import train_test_split
+try:
+    from sklearn.compose import ColumnTransformer
+except ImportError:
+    ColumnTransformer = None
+from sklearn.pipeline import Pipeline
+from sklearn.preprocessing import MinMaxScaler, OneHotEncoder
+from skl2onnx import convert_sklearn
+from skl2onnx.common.data_types import (
+    BooleanTensorType, FloatTensorType,
+    Int64TensorType, StringTensorType)
+
+
+def _column_tranformer_fitted_from_df(data):
+    def transformer_for_column(column: pd.Series):
+        if column.dtype in ['float64', 'float32', 'int64']:
+            return MinMaxScaler()
+        if column.dtype in ['bool']:
+            return 'passthrough'
+        if column.dtype in ['O']:
+            try:
+                return OneHotEncoder(drop='first')
+            except TypeError:
+                # older version of scikit-learn
+                return OneHotEncoder()
+        raise ValueError(
+            'Unexpected column dtype for {column.name}:{column.dtype}'.format(
+                column=column))
+
+    return ColumnTransformer(
+        [(col, transformer_for_column(
+            data[col]), [col]) for col in data.columns],
+        remainder='drop'
+    ).fit(data)
+
+
+def _convert_dataframe_schema(data):
+    def type_for_column(column: pd.Series):
+        if column.dtype in ['float64', 'float32']:
+            return FloatTensorType([None, 1])
+        if column.dtype in ['int64']:
+            return Int64TensorType([None, 1])
+        if column.dtype in ['bool']:
+            return BooleanTensorType([None, 1])
+        if column.dtype in ['O']:
+            return StringTensorType([None, 1])
+        raise ValueError(
+            'Unexpected column dtype for {column.name}:{column.dtype}'.format(
+                column=column))
+    return [(col, type_for_column(data[col])) for col in data.columns]
+
+
+def _predict(session: rt.InferenceSession, data: pd.DataFrame) -> pd.Series:
+    def _correctly_typed_column(column: pd.Series) -> pd.Series:
+        if column.dtype in ['float64']:
+            return column.astype(np.float32)
+        return column
+
+    def _correctly_shaped_values(values):
+        return values.reshape((values.shape[0], 1))
+
+    inputs = {
+        c: _correctly_shaped_values(_correctly_typed_column(data[c]).values)
+        for c in data.columns
+    }
+
+    return pd.Series(
+        session.run(None, inputs)[0].reshape(-1),
+        index=data.index
+    )
+
+
+class TestSklearnPipeline(unittest.TestCase):
+
+    @unittest.skipIf(ColumnTransformer is None, reason="too old scikit-learn")
+    def test_concat(self):
+        data = os.path.join(os.path.dirname(__file__),
+                            "datasets", "small_titanic.csv")
+        data = pd.read_csv(data)
+        data['female'] = data['sex'] == 'female'
+        data = data[['age', 'fare', 'female', 'embarked',
+                     'pclass', 'survived']]
+
+        for col in data:
+            dtype = data[col].dtype
+            if dtype in ['float64', 'float32']:
+                data[col].fillna(0., inplace=True)
+            if dtype in ['int64']:
+                data[col].fillna(0, inplace=True)
+            elif dtype in ['O']:
+                data[col].fillna('N/A', inplace=True)
+
+        full_df = data.drop('survived', axis=1)
+        full_labels = data['survived']
+
+        train_df, test_df, train_labels, test_labels = train_test_split(
+            full_df, full_labels, test_size=.2)
+
+        col_transformer = _column_tranformer_fitted_from_df(full_df)
+
+        regressor = DecisionTreeRegressor()
+        regressor.fit(
+            col_transformer.transform(train_df),
+            train_labels)
+        model = Pipeline(
+            steps=[('preprocessor', col_transformer),
+                   ('regressor', regressor)])
+
+        initial_types = _convert_dataframe_schema(full_df)
+        itypes = set(_[1].__class__ for _ in initial_types)
+        self.assertIn(BooleanTensorType, itypes)
+        self.assertIn(FloatTensorType, itypes)
+        onnx = convert_sklearn(model, initial_types=initial_types)
+
+        session = rt.InferenceSession(onnx.SerializeToString())
+
+        pred_skl = model.predict(test_df)
+        pred_onx = _predict(session, test_df)
+
+        diff = np.sort(
+            np.abs(np.squeeze(pred_skl) - np.squeeze(pred_onx)))
+        self.assertEqual(diff[0], diff[-1])
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/tests/test_sklearn_feature_union.py b/tests/test_sklearn_feature_union.py
--- a/tests/test_sklearn_feature_union.py
+++ b/tests/test_sklearn_feature_union.py
@@ -5,18 +5,23 @@
 # --------------------------------------------------------------------------
 
 import unittest
+from distutils.version import StrictVersion
 import numpy as np
 from sklearn.datasets import load_digits, load_iris
 from sklearn.decomposition import PCA, TruncatedSVD
 from sklearn.model_selection import train_test_split
 from sklearn.pipeline import FeatureUnion
 from sklearn.preprocessing import StandardScaler, MinMaxScaler
+from onnxruntime import __version__ as ort_version
 from skl2onnx import convert_sklearn
 from skl2onnx.common.data_types import FloatTensorType, Int64TensorType
 from test_utils import dump_data_and_model
 
 
 class TestSklearnAdaBoostModels(unittest.TestCase):
+    @unittest.skipIf(
+        StrictVersion(ort_version) <= StrictVersion('0.4.0'),
+        reason="onnxruntime too old")
     def test_feature_union_default(self):
         data = load_iris()
         X, y = data.data, data.target
@@ -34,6 +39,9 @@ def test_feature_union_default(self):
                             model_onnx,
                             basename="SklearnFeatureUnionDefault")
 
+    @unittest.skipIf(
+        StrictVersion(ort_version) <= StrictVersion('0.4.0'),
+        reason="onnxruntime too old")
     def test_feature_union_transformer_weights_0(self):
         data = load_iris()
         X, y = data.data, data.target
diff --git a/tests/test_sklearn_pipeline.py b/tests/test_sklearn_pipeline.py
--- a/tests/test_sklearn_pipeline.py
+++ b/tests/test_sklearn_pipeline.py
@@ -105,6 +105,9 @@ def test_combine_inputs(self):
             basename="SklearnPipelineScaler11",
         )
 
+    @unittest.skipIf(
+        StrictVersion(ort_version) <= StrictVersion('0.4.0'),
+        reason="onnxruntime too old")
     def test_combine_inputs_union_in_pipeline(self):
         from sklearn.preprocessing import StandardScaler
         from sklearn.pipeline import Pipeline