test_column_transformer.py 74 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231
  1. """
  2. Test the ColumnTransformer.
  3. """
  4. import pickle
  5. import re
  6. import numpy as np
  7. import pytest
  8. from numpy.testing import assert_allclose
  9. from scipy import sparse
  10. from sklearn.base import BaseEstimator, TransformerMixin
  11. from sklearn.compose import (
  12. ColumnTransformer,
  13. make_column_selector,
  14. make_column_transformer,
  15. )
  16. from sklearn.exceptions import NotFittedError
  17. from sklearn.feature_selection import VarianceThreshold
  18. from sklearn.preprocessing import (
  19. FunctionTransformer,
  20. Normalizer,
  21. OneHotEncoder,
  22. StandardScaler,
  23. )
  24. from sklearn.utils._testing import (
  25. assert_allclose_dense_sparse,
  26. assert_almost_equal,
  27. assert_array_equal,
  28. )
  29. class Trans(TransformerMixin, BaseEstimator):
  30. def fit(self, X, y=None):
  31. return self
  32. def transform(self, X, y=None):
  33. # 1D Series -> 2D DataFrame
  34. if hasattr(X, "to_frame"):
  35. return X.to_frame()
  36. # 1D array -> 2D array
  37. if X.ndim == 1:
  38. return np.atleast_2d(X).T
  39. return X
  40. class DoubleTrans(BaseEstimator):
  41. def fit(self, X, y=None):
  42. return self
  43. def transform(self, X):
  44. return 2 * X
  45. class SparseMatrixTrans(BaseEstimator):
  46. def fit(self, X, y=None):
  47. return self
  48. def transform(self, X, y=None):
  49. n_samples = len(X)
  50. return sparse.eye(n_samples, n_samples).tocsr()
  51. class TransNo2D(BaseEstimator):
  52. def fit(self, X, y=None):
  53. return self
  54. def transform(self, X, y=None):
  55. return X
  56. class TransRaise(BaseEstimator):
  57. def fit(self, X, y=None):
  58. raise ValueError("specific message")
  59. def transform(self, X, y=None):
  60. raise ValueError("specific message")
  61. def test_column_transformer():
  62. X_array = np.array([[0, 1, 2], [2, 4, 6]]).T
  63. X_res_first1D = np.array([0, 1, 2])
  64. X_res_second1D = np.array([2, 4, 6])
  65. X_res_first = X_res_first1D.reshape(-1, 1)
  66. X_res_both = X_array
  67. cases = [
  68. # single column 1D / 2D
  69. (0, X_res_first),
  70. ([0], X_res_first),
  71. # list-like
  72. ([0, 1], X_res_both),
  73. (np.array([0, 1]), X_res_both),
  74. # slice
  75. (slice(0, 1), X_res_first),
  76. (slice(0, 2), X_res_both),
  77. # boolean mask
  78. (np.array([True, False]), X_res_first),
  79. ([True, False], X_res_first),
  80. (np.array([True, True]), X_res_both),
  81. ([True, True], X_res_both),
  82. ]
  83. for selection, res in cases:
  84. ct = ColumnTransformer([("trans", Trans(), selection)], remainder="drop")
  85. assert_array_equal(ct.fit_transform(X_array), res)
  86. assert_array_equal(ct.fit(X_array).transform(X_array), res)
  87. # callable that returns any of the allowed specifiers
  88. ct = ColumnTransformer(
  89. [("trans", Trans(), lambda x: selection)], remainder="drop"
  90. )
  91. assert_array_equal(ct.fit_transform(X_array), res)
  92. assert_array_equal(ct.fit(X_array).transform(X_array), res)
  93. ct = ColumnTransformer([("trans1", Trans(), [0]), ("trans2", Trans(), [1])])
  94. assert_array_equal(ct.fit_transform(X_array), X_res_both)
  95. assert_array_equal(ct.fit(X_array).transform(X_array), X_res_both)
  96. assert len(ct.transformers_) == 2
  97. # test with transformer_weights
  98. transformer_weights = {"trans1": 0.1, "trans2": 10}
  99. both = ColumnTransformer(
  100. [("trans1", Trans(), [0]), ("trans2", Trans(), [1])],
  101. transformer_weights=transformer_weights,
  102. )
  103. res = np.vstack(
  104. [
  105. transformer_weights["trans1"] * X_res_first1D,
  106. transformer_weights["trans2"] * X_res_second1D,
  107. ]
  108. ).T
  109. assert_array_equal(both.fit_transform(X_array), res)
  110. assert_array_equal(both.fit(X_array).transform(X_array), res)
  111. assert len(both.transformers_) == 2
  112. both = ColumnTransformer(
  113. [("trans", Trans(), [0, 1])], transformer_weights={"trans": 0.1}
  114. )
  115. assert_array_equal(both.fit_transform(X_array), 0.1 * X_res_both)
  116. assert_array_equal(both.fit(X_array).transform(X_array), 0.1 * X_res_both)
  117. assert len(both.transformers_) == 1
  118. def test_column_transformer_tuple_transformers_parameter():
  119. X_array = np.array([[0, 1, 2], [2, 4, 6]]).T
  120. transformers = [("trans1", Trans(), [0]), ("trans2", Trans(), [1])]
  121. ct_with_list = ColumnTransformer(transformers)
  122. ct_with_tuple = ColumnTransformer(tuple(transformers))
  123. assert_array_equal(
  124. ct_with_list.fit_transform(X_array), ct_with_tuple.fit_transform(X_array)
  125. )
  126. assert_array_equal(
  127. ct_with_list.fit(X_array).transform(X_array),
  128. ct_with_tuple.fit(X_array).transform(X_array),
  129. )
  130. def test_column_transformer_dataframe():
  131. pd = pytest.importorskip("pandas")
  132. X_array = np.array([[0, 1, 2], [2, 4, 6]]).T
  133. X_df = pd.DataFrame(X_array, columns=["first", "second"])
  134. X_res_first = np.array([0, 1, 2]).reshape(-1, 1)
  135. X_res_both = X_array
  136. cases = [
  137. # String keys: label based
  138. # scalar
  139. ("first", X_res_first),
  140. # list
  141. (["first"], X_res_first),
  142. (["first", "second"], X_res_both),
  143. # slice
  144. (slice("first", "second"), X_res_both),
  145. # int keys: positional
  146. # scalar
  147. (0, X_res_first),
  148. # list
  149. ([0], X_res_first),
  150. ([0, 1], X_res_both),
  151. (np.array([0, 1]), X_res_both),
  152. # slice
  153. (slice(0, 1), X_res_first),
  154. (slice(0, 2), X_res_both),
  155. # boolean mask
  156. (np.array([True, False]), X_res_first),
  157. (pd.Series([True, False], index=["first", "second"]), X_res_first),
  158. ([True, False], X_res_first),
  159. ]
  160. for selection, res in cases:
  161. ct = ColumnTransformer([("trans", Trans(), selection)], remainder="drop")
  162. assert_array_equal(ct.fit_transform(X_df), res)
  163. assert_array_equal(ct.fit(X_df).transform(X_df), res)
  164. # callable that returns any of the allowed specifiers
  165. ct = ColumnTransformer(
  166. [("trans", Trans(), lambda X: selection)], remainder="drop"
  167. )
  168. assert_array_equal(ct.fit_transform(X_df), res)
  169. assert_array_equal(ct.fit(X_df).transform(X_df), res)
  170. ct = ColumnTransformer(
  171. [("trans1", Trans(), ["first"]), ("trans2", Trans(), ["second"])]
  172. )
  173. assert_array_equal(ct.fit_transform(X_df), X_res_both)
  174. assert_array_equal(ct.fit(X_df).transform(X_df), X_res_both)
  175. assert len(ct.transformers_) == 2
  176. assert ct.transformers_[-1][0] != "remainder"
  177. ct = ColumnTransformer([("trans1", Trans(), [0]), ("trans2", Trans(), [1])])
  178. assert_array_equal(ct.fit_transform(X_df), X_res_both)
  179. assert_array_equal(ct.fit(X_df).transform(X_df), X_res_both)
  180. assert len(ct.transformers_) == 2
  181. assert ct.transformers_[-1][0] != "remainder"
  182. # test with transformer_weights
  183. transformer_weights = {"trans1": 0.1, "trans2": 10}
  184. both = ColumnTransformer(
  185. [("trans1", Trans(), ["first"]), ("trans2", Trans(), ["second"])],
  186. transformer_weights=transformer_weights,
  187. )
  188. res = np.vstack(
  189. [
  190. transformer_weights["trans1"] * X_df["first"],
  191. transformer_weights["trans2"] * X_df["second"],
  192. ]
  193. ).T
  194. assert_array_equal(both.fit_transform(X_df), res)
  195. assert_array_equal(both.fit(X_df).transform(X_df), res)
  196. assert len(both.transformers_) == 2
  197. assert both.transformers_[-1][0] != "remainder"
  198. # test multiple columns
  199. both = ColumnTransformer(
  200. [("trans", Trans(), ["first", "second"])], transformer_weights={"trans": 0.1}
  201. )
  202. assert_array_equal(both.fit_transform(X_df), 0.1 * X_res_both)
  203. assert_array_equal(both.fit(X_df).transform(X_df), 0.1 * X_res_both)
  204. assert len(both.transformers_) == 1
  205. assert both.transformers_[-1][0] != "remainder"
  206. both = ColumnTransformer(
  207. [("trans", Trans(), [0, 1])], transformer_weights={"trans": 0.1}
  208. )
  209. assert_array_equal(both.fit_transform(X_df), 0.1 * X_res_both)
  210. assert_array_equal(both.fit(X_df).transform(X_df), 0.1 * X_res_both)
  211. assert len(both.transformers_) == 1
  212. assert both.transformers_[-1][0] != "remainder"
  213. # ensure pandas object is passed through
  214. class TransAssert(BaseEstimator):
  215. def __init__(self, expected_type_transform):
  216. self.expected_type_transform = expected_type_transform
  217. def fit(self, X, y=None):
  218. return self
  219. def transform(self, X, y=None):
  220. assert isinstance(X, self.expected_type_transform)
  221. if isinstance(X, pd.Series):
  222. X = X.to_frame()
  223. return X
  224. ct = ColumnTransformer(
  225. [("trans", TransAssert(expected_type_transform=pd.Series), "first")],
  226. remainder="drop",
  227. )
  228. ct.fit_transform(X_df)
  229. ct = ColumnTransformer(
  230. [
  231. (
  232. "trans",
  233. TransAssert(expected_type_transform=pd.DataFrame),
  234. ["first", "second"],
  235. )
  236. ]
  237. )
  238. ct.fit_transform(X_df)
  239. # integer column spec + integer column names -> still use positional
  240. X_df2 = X_df.copy()
  241. X_df2.columns = [1, 0]
  242. ct = ColumnTransformer([("trans", Trans(), 0)], remainder="drop")
  243. assert_array_equal(ct.fit_transform(X_df2), X_res_first)
  244. assert_array_equal(ct.fit(X_df2).transform(X_df2), X_res_first)
  245. assert len(ct.transformers_) == 2
  246. assert ct.transformers_[-1][0] == "remainder"
  247. assert ct.transformers_[-1][1] == "drop"
  248. assert_array_equal(ct.transformers_[-1][2], [1])
  249. @pytest.mark.parametrize("pandas", [True, False], ids=["pandas", "numpy"])
  250. @pytest.mark.parametrize(
  251. "column_selection",
  252. [[], np.array([False, False]), [False, False]],
  253. ids=["list", "bool", "bool_int"],
  254. )
  255. @pytest.mark.parametrize("callable_column", [False, True])
  256. def test_column_transformer_empty_columns(pandas, column_selection, callable_column):
  257. # test case that ensures that the column transformer does also work when
  258. # a given transformer doesn't have any columns to work on
  259. X_array = np.array([[0, 1, 2], [2, 4, 6]]).T
  260. X_res_both = X_array
  261. if pandas:
  262. pd = pytest.importorskip("pandas")
  263. X = pd.DataFrame(X_array, columns=["first", "second"])
  264. else:
  265. X = X_array
  266. if callable_column:
  267. column = lambda X: column_selection # noqa
  268. else:
  269. column = column_selection
  270. ct = ColumnTransformer(
  271. [("trans1", Trans(), [0, 1]), ("trans2", TransRaise(), column)]
  272. )
  273. assert_array_equal(ct.fit_transform(X), X_res_both)
  274. assert_array_equal(ct.fit(X).transform(X), X_res_both)
  275. assert len(ct.transformers_) == 2
  276. assert isinstance(ct.transformers_[1][1], TransRaise)
  277. ct = ColumnTransformer(
  278. [("trans1", TransRaise(), column), ("trans2", Trans(), [0, 1])]
  279. )
  280. assert_array_equal(ct.fit_transform(X), X_res_both)
  281. assert_array_equal(ct.fit(X).transform(X), X_res_both)
  282. assert len(ct.transformers_) == 2
  283. assert isinstance(ct.transformers_[0][1], TransRaise)
  284. ct = ColumnTransformer([("trans", TransRaise(), column)], remainder="passthrough")
  285. assert_array_equal(ct.fit_transform(X), X_res_both)
  286. assert_array_equal(ct.fit(X).transform(X), X_res_both)
  287. assert len(ct.transformers_) == 2 # including remainder
  288. assert isinstance(ct.transformers_[0][1], TransRaise)
  289. fixture = np.array([[], [], []])
  290. ct = ColumnTransformer([("trans", TransRaise(), column)], remainder="drop")
  291. assert_array_equal(ct.fit_transform(X), fixture)
  292. assert_array_equal(ct.fit(X).transform(X), fixture)
  293. assert len(ct.transformers_) == 2 # including remainder
  294. assert isinstance(ct.transformers_[0][1], TransRaise)
  295. def test_column_transformer_output_indices():
  296. # Checks for the output_indices_ attribute
  297. X_array = np.arange(6).reshape(3, 2)
  298. ct = ColumnTransformer([("trans1", Trans(), [0]), ("trans2", Trans(), [1])])
  299. X_trans = ct.fit_transform(X_array)
  300. assert ct.output_indices_ == {
  301. "trans1": slice(0, 1),
  302. "trans2": slice(1, 2),
  303. "remainder": slice(0, 0),
  304. }
  305. assert_array_equal(X_trans[:, [0]], X_trans[:, ct.output_indices_["trans1"]])
  306. assert_array_equal(X_trans[:, [1]], X_trans[:, ct.output_indices_["trans2"]])
  307. # test with transformer_weights and multiple columns
  308. ct = ColumnTransformer(
  309. [("trans", Trans(), [0, 1])], transformer_weights={"trans": 0.1}
  310. )
  311. X_trans = ct.fit_transform(X_array)
  312. assert ct.output_indices_ == {"trans": slice(0, 2), "remainder": slice(0, 0)}
  313. assert_array_equal(X_trans[:, [0, 1]], X_trans[:, ct.output_indices_["trans"]])
  314. assert_array_equal(X_trans[:, []], X_trans[:, ct.output_indices_["remainder"]])
  315. # test case that ensures that the attribute does also work when
  316. # a given transformer doesn't have any columns to work on
  317. ct = ColumnTransformer([("trans1", Trans(), [0, 1]), ("trans2", TransRaise(), [])])
  318. X_trans = ct.fit_transform(X_array)
  319. assert ct.output_indices_ == {
  320. "trans1": slice(0, 2),
  321. "trans2": slice(0, 0),
  322. "remainder": slice(0, 0),
  323. }
  324. assert_array_equal(X_trans[:, [0, 1]], X_trans[:, ct.output_indices_["trans1"]])
  325. assert_array_equal(X_trans[:, []], X_trans[:, ct.output_indices_["trans2"]])
  326. assert_array_equal(X_trans[:, []], X_trans[:, ct.output_indices_["remainder"]])
  327. ct = ColumnTransformer([("trans", TransRaise(), [])], remainder="passthrough")
  328. X_trans = ct.fit_transform(X_array)
  329. assert ct.output_indices_ == {"trans": slice(0, 0), "remainder": slice(0, 2)}
  330. assert_array_equal(X_trans[:, []], X_trans[:, ct.output_indices_["trans"]])
  331. assert_array_equal(X_trans[:, [0, 1]], X_trans[:, ct.output_indices_["remainder"]])
  332. def test_column_transformer_output_indices_df():
  333. # Checks for the output_indices_ attribute with data frames
  334. pd = pytest.importorskip("pandas")
  335. X_df = pd.DataFrame(np.arange(6).reshape(3, 2), columns=["first", "second"])
  336. ct = ColumnTransformer(
  337. [("trans1", Trans(), ["first"]), ("trans2", Trans(), ["second"])]
  338. )
  339. X_trans = ct.fit_transform(X_df)
  340. assert ct.output_indices_ == {
  341. "trans1": slice(0, 1),
  342. "trans2": slice(1, 2),
  343. "remainder": slice(0, 0),
  344. }
  345. assert_array_equal(X_trans[:, [0]], X_trans[:, ct.output_indices_["trans1"]])
  346. assert_array_equal(X_trans[:, [1]], X_trans[:, ct.output_indices_["trans2"]])
  347. assert_array_equal(X_trans[:, []], X_trans[:, ct.output_indices_["remainder"]])
  348. ct = ColumnTransformer([("trans1", Trans(), [0]), ("trans2", Trans(), [1])])
  349. X_trans = ct.fit_transform(X_df)
  350. assert ct.output_indices_ == {
  351. "trans1": slice(0, 1),
  352. "trans2": slice(1, 2),
  353. "remainder": slice(0, 0),
  354. }
  355. assert_array_equal(X_trans[:, [0]], X_trans[:, ct.output_indices_["trans1"]])
  356. assert_array_equal(X_trans[:, [1]], X_trans[:, ct.output_indices_["trans2"]])
  357. assert_array_equal(X_trans[:, []], X_trans[:, ct.output_indices_["remainder"]])
  358. def test_column_transformer_sparse_array():
  359. X_sparse = sparse.eye(3, 2).tocsr()
  360. # no distinction between 1D and 2D
  361. X_res_first = X_sparse[:, 0]
  362. X_res_both = X_sparse
  363. for col in [0, [0], slice(0, 1)]:
  364. for remainder, res in [("drop", X_res_first), ("passthrough", X_res_both)]:
  365. ct = ColumnTransformer(
  366. [("trans", Trans(), col)], remainder=remainder, sparse_threshold=0.8
  367. )
  368. assert sparse.issparse(ct.fit_transform(X_sparse))
  369. assert_allclose_dense_sparse(ct.fit_transform(X_sparse), res)
  370. assert_allclose_dense_sparse(ct.fit(X_sparse).transform(X_sparse), res)
  371. for col in [[0, 1], slice(0, 2)]:
  372. ct = ColumnTransformer([("trans", Trans(), col)], sparse_threshold=0.8)
  373. assert sparse.issparse(ct.fit_transform(X_sparse))
  374. assert_allclose_dense_sparse(ct.fit_transform(X_sparse), X_res_both)
  375. assert_allclose_dense_sparse(ct.fit(X_sparse).transform(X_sparse), X_res_both)
  376. def test_column_transformer_list():
  377. X_list = [[1, float("nan"), "a"], [0, 0, "b"]]
  378. expected_result = np.array(
  379. [
  380. [1, float("nan"), 1, 0],
  381. [-1, 0, 0, 1],
  382. ]
  383. )
  384. ct = ColumnTransformer(
  385. [
  386. ("numerical", StandardScaler(), [0, 1]),
  387. ("categorical", OneHotEncoder(), [2]),
  388. ]
  389. )
  390. assert_array_equal(ct.fit_transform(X_list), expected_result)
  391. assert_array_equal(ct.fit(X_list).transform(X_list), expected_result)
  392. def test_column_transformer_sparse_stacking():
  393. X_array = np.array([[0, 1, 2], [2, 4, 6]]).T
  394. col_trans = ColumnTransformer(
  395. [("trans1", Trans(), [0]), ("trans2", SparseMatrixTrans(), 1)],
  396. sparse_threshold=0.8,
  397. )
  398. col_trans.fit(X_array)
  399. X_trans = col_trans.transform(X_array)
  400. assert sparse.issparse(X_trans)
  401. assert X_trans.shape == (X_trans.shape[0], X_trans.shape[0] + 1)
  402. assert_array_equal(X_trans.toarray()[:, 1:], np.eye(X_trans.shape[0]))
  403. assert len(col_trans.transformers_) == 2
  404. assert col_trans.transformers_[-1][0] != "remainder"
  405. col_trans = ColumnTransformer(
  406. [("trans1", Trans(), [0]), ("trans2", SparseMatrixTrans(), 1)],
  407. sparse_threshold=0.1,
  408. )
  409. col_trans.fit(X_array)
  410. X_trans = col_trans.transform(X_array)
  411. assert not sparse.issparse(X_trans)
  412. assert X_trans.shape == (X_trans.shape[0], X_trans.shape[0] + 1)
  413. assert_array_equal(X_trans[:, 1:], np.eye(X_trans.shape[0]))
  414. def test_column_transformer_mixed_cols_sparse():
  415. df = np.array([["a", 1, True], ["b", 2, False]], dtype="O")
  416. ct = make_column_transformer(
  417. (OneHotEncoder(), [0]), ("passthrough", [1, 2]), sparse_threshold=1.0
  418. )
  419. # this shouldn't fail, since boolean can be coerced into a numeric
  420. # See: https://github.com/scikit-learn/scikit-learn/issues/11912
  421. X_trans = ct.fit_transform(df)
  422. assert X_trans.getformat() == "csr"
  423. assert_array_equal(X_trans.toarray(), np.array([[1, 0, 1, 1], [0, 1, 2, 0]]))
  424. ct = make_column_transformer(
  425. (OneHotEncoder(), [0]), ("passthrough", [0]), sparse_threshold=1.0
  426. )
  427. with pytest.raises(ValueError, match="For a sparse output, all columns should"):
  428. # this fails since strings `a` and `b` cannot be
  429. # coerced into a numeric.
  430. ct.fit_transform(df)
  431. def test_column_transformer_sparse_threshold():
  432. X_array = np.array([["a", "b"], ["A", "B"]], dtype=object).T
  433. # above data has sparsity of 4 / 8 = 0.5
  434. # apply threshold even if all sparse
  435. col_trans = ColumnTransformer(
  436. [("trans1", OneHotEncoder(), [0]), ("trans2", OneHotEncoder(), [1])],
  437. sparse_threshold=0.2,
  438. )
  439. res = col_trans.fit_transform(X_array)
  440. assert not sparse.issparse(res)
  441. assert not col_trans.sparse_output_
  442. # mixed -> sparsity of (4 + 2) / 8 = 0.75
  443. for thres in [0.75001, 1]:
  444. col_trans = ColumnTransformer(
  445. [
  446. ("trans1", OneHotEncoder(sparse_output=True), [0]),
  447. ("trans2", OneHotEncoder(sparse_output=False), [1]),
  448. ],
  449. sparse_threshold=thres,
  450. )
  451. res = col_trans.fit_transform(X_array)
  452. assert sparse.issparse(res)
  453. assert col_trans.sparse_output_
  454. for thres in [0.75, 0]:
  455. col_trans = ColumnTransformer(
  456. [
  457. ("trans1", OneHotEncoder(sparse_output=True), [0]),
  458. ("trans2", OneHotEncoder(sparse_output=False), [1]),
  459. ],
  460. sparse_threshold=thres,
  461. )
  462. res = col_trans.fit_transform(X_array)
  463. assert not sparse.issparse(res)
  464. assert not col_trans.sparse_output_
  465. # if nothing is sparse -> no sparse
  466. for thres in [0.33, 0, 1]:
  467. col_trans = ColumnTransformer(
  468. [
  469. ("trans1", OneHotEncoder(sparse_output=False), [0]),
  470. ("trans2", OneHotEncoder(sparse_output=False), [1]),
  471. ],
  472. sparse_threshold=thres,
  473. )
  474. res = col_trans.fit_transform(X_array)
  475. assert not sparse.issparse(res)
  476. assert not col_trans.sparse_output_
  477. def test_column_transformer_error_msg_1D():
  478. X_array = np.array([[0.0, 1.0, 2.0], [2.0, 4.0, 6.0]]).T
  479. col_trans = ColumnTransformer([("trans", StandardScaler(), 0)])
  480. msg = "1D data passed to a transformer"
  481. with pytest.raises(ValueError, match=msg):
  482. col_trans.fit(X_array)
  483. with pytest.raises(ValueError, match=msg):
  484. col_trans.fit_transform(X_array)
  485. col_trans = ColumnTransformer([("trans", TransRaise(), 0)])
  486. for func in [col_trans.fit, col_trans.fit_transform]:
  487. with pytest.raises(ValueError, match="specific message"):
  488. func(X_array)
  489. def test_2D_transformer_output():
  490. X_array = np.array([[0, 1, 2], [2, 4, 6]]).T
  491. # if one transformer is dropped, test that name is still correct
  492. ct = ColumnTransformer([("trans1", "drop", 0), ("trans2", TransNo2D(), 1)])
  493. msg = "the 'trans2' transformer should be 2D"
  494. with pytest.raises(ValueError, match=msg):
  495. ct.fit_transform(X_array)
  496. # because fit is also doing transform, this raises already on fit
  497. with pytest.raises(ValueError, match=msg):
  498. ct.fit(X_array)
  499. def test_2D_transformer_output_pandas():
  500. pd = pytest.importorskip("pandas")
  501. X_array = np.array([[0, 1, 2], [2, 4, 6]]).T
  502. X_df = pd.DataFrame(X_array, columns=["col1", "col2"])
  503. # if one transformer is dropped, test that name is still correct
  504. ct = ColumnTransformer([("trans1", TransNo2D(), "col1")])
  505. msg = "the 'trans1' transformer should be 2D"
  506. with pytest.raises(ValueError, match=msg):
  507. ct.fit_transform(X_df)
  508. # because fit is also doing transform, this raises already on fit
  509. with pytest.raises(ValueError, match=msg):
  510. ct.fit(X_df)
  511. @pytest.mark.parametrize("remainder", ["drop", "passthrough"])
  512. def test_column_transformer_invalid_columns(remainder):
  513. X_array = np.array([[0, 1, 2], [2, 4, 6]]).T
  514. # general invalid
  515. for col in [1.5, ["string", 1], slice(1, "s"), np.array([1.0])]:
  516. ct = ColumnTransformer([("trans", Trans(), col)], remainder=remainder)
  517. with pytest.raises(ValueError, match="No valid specification"):
  518. ct.fit(X_array)
  519. # invalid for arrays
  520. for col in ["string", ["string", "other"], slice("a", "b")]:
  521. ct = ColumnTransformer([("trans", Trans(), col)], remainder=remainder)
  522. with pytest.raises(ValueError, match="Specifying the columns"):
  523. ct.fit(X_array)
  524. # transformed n_features does not match fitted n_features
  525. col = [0, 1]
  526. ct = ColumnTransformer([("trans", Trans(), col)], remainder=remainder)
  527. ct.fit(X_array)
  528. X_array_more = np.array([[0, 1, 2], [2, 4, 6], [3, 6, 9]]).T
  529. msg = "X has 3 features, but ColumnTransformer is expecting 2 features as input."
  530. with pytest.raises(ValueError, match=msg):
  531. ct.transform(X_array_more)
  532. X_array_fewer = np.array(
  533. [
  534. [0, 1, 2],
  535. ]
  536. ).T
  537. err_msg = (
  538. "X has 1 features, but ColumnTransformer is expecting 2 features as input."
  539. )
  540. with pytest.raises(ValueError, match=err_msg):
  541. ct.transform(X_array_fewer)
  542. def test_column_transformer_invalid_transformer():
  543. class NoTrans(BaseEstimator):
  544. def fit(self, X, y=None):
  545. return self
  546. def predict(self, X):
  547. return X
  548. X_array = np.array([[0, 1, 2], [2, 4, 6]]).T
  549. ct = ColumnTransformer([("trans", NoTrans(), [0])])
  550. msg = "All estimators should implement fit and transform"
  551. with pytest.raises(TypeError, match=msg):
  552. ct.fit(X_array)
  553. def test_make_column_transformer():
  554. scaler = StandardScaler()
  555. norm = Normalizer()
  556. ct = make_column_transformer((scaler, "first"), (norm, ["second"]))
  557. names, transformers, columns = zip(*ct.transformers)
  558. assert names == ("standardscaler", "normalizer")
  559. assert transformers == (scaler, norm)
  560. assert columns == ("first", ["second"])
  561. def test_make_column_transformer_pandas():
  562. pd = pytest.importorskip("pandas")
  563. X_array = np.array([[0, 1, 2], [2, 4, 6]]).T
  564. X_df = pd.DataFrame(X_array, columns=["first", "second"])
  565. norm = Normalizer()
  566. ct1 = ColumnTransformer([("norm", Normalizer(), X_df.columns)])
  567. ct2 = make_column_transformer((norm, X_df.columns))
  568. assert_almost_equal(ct1.fit_transform(X_df), ct2.fit_transform(X_df))
  569. def test_make_column_transformer_kwargs():
  570. scaler = StandardScaler()
  571. norm = Normalizer()
  572. ct = make_column_transformer(
  573. (scaler, "first"),
  574. (norm, ["second"]),
  575. n_jobs=3,
  576. remainder="drop",
  577. sparse_threshold=0.5,
  578. )
  579. assert (
  580. ct.transformers
  581. == make_column_transformer((scaler, "first"), (norm, ["second"])).transformers
  582. )
  583. assert ct.n_jobs == 3
  584. assert ct.remainder == "drop"
  585. assert ct.sparse_threshold == 0.5
  586. # invalid keyword parameters should raise an error message
  587. msg = re.escape(
  588. "make_column_transformer() got an unexpected "
  589. "keyword argument 'transformer_weights'"
  590. )
  591. with pytest.raises(TypeError, match=msg):
  592. make_column_transformer(
  593. (scaler, "first"),
  594. (norm, ["second"]),
  595. transformer_weights={"pca": 10, "Transf": 1},
  596. )
  597. def test_make_column_transformer_remainder_transformer():
  598. scaler = StandardScaler()
  599. norm = Normalizer()
  600. remainder = StandardScaler()
  601. ct = make_column_transformer(
  602. (scaler, "first"), (norm, ["second"]), remainder=remainder
  603. )
  604. assert ct.remainder == remainder
  605. def test_column_transformer_get_set_params():
  606. ct = ColumnTransformer(
  607. [("trans1", StandardScaler(), [0]), ("trans2", StandardScaler(), [1])]
  608. )
  609. exp = {
  610. "n_jobs": None,
  611. "remainder": "drop",
  612. "sparse_threshold": 0.3,
  613. "trans1": ct.transformers[0][1],
  614. "trans1__copy": True,
  615. "trans1__with_mean": True,
  616. "trans1__with_std": True,
  617. "trans2": ct.transformers[1][1],
  618. "trans2__copy": True,
  619. "trans2__with_mean": True,
  620. "trans2__with_std": True,
  621. "transformers": ct.transformers,
  622. "transformer_weights": None,
  623. "verbose_feature_names_out": True,
  624. "verbose": False,
  625. }
  626. assert ct.get_params() == exp
  627. ct.set_params(trans1__with_mean=False)
  628. assert not ct.get_params()["trans1__with_mean"]
  629. ct.set_params(trans1="passthrough")
  630. exp = {
  631. "n_jobs": None,
  632. "remainder": "drop",
  633. "sparse_threshold": 0.3,
  634. "trans1": "passthrough",
  635. "trans2": ct.transformers[1][1],
  636. "trans2__copy": True,
  637. "trans2__with_mean": True,
  638. "trans2__with_std": True,
  639. "transformers": ct.transformers,
  640. "transformer_weights": None,
  641. "verbose_feature_names_out": True,
  642. "verbose": False,
  643. }
  644. assert ct.get_params() == exp
  645. def test_column_transformer_named_estimators():
  646. X_array = np.array([[0.0, 1.0, 2.0], [2.0, 4.0, 6.0]]).T
  647. ct = ColumnTransformer(
  648. [
  649. ("trans1", StandardScaler(), [0]),
  650. ("trans2", StandardScaler(with_std=False), [1]),
  651. ]
  652. )
  653. assert not hasattr(ct, "transformers_")
  654. ct.fit(X_array)
  655. assert hasattr(ct, "transformers_")
  656. assert isinstance(ct.named_transformers_["trans1"], StandardScaler)
  657. assert isinstance(ct.named_transformers_.trans1, StandardScaler)
  658. assert isinstance(ct.named_transformers_["trans2"], StandardScaler)
  659. assert isinstance(ct.named_transformers_.trans2, StandardScaler)
  660. assert not ct.named_transformers_.trans2.with_std
  661. # check it are fitted transformers
  662. assert ct.named_transformers_.trans1.mean_ == 1.0
  663. def test_column_transformer_cloning():
  664. X_array = np.array([[0.0, 1.0, 2.0], [2.0, 4.0, 6.0]]).T
  665. ct = ColumnTransformer([("trans", StandardScaler(), [0])])
  666. ct.fit(X_array)
  667. assert not hasattr(ct.transformers[0][1], "mean_")
  668. assert hasattr(ct.transformers_[0][1], "mean_")
  669. ct = ColumnTransformer([("trans", StandardScaler(), [0])])
  670. ct.fit_transform(X_array)
  671. assert not hasattr(ct.transformers[0][1], "mean_")
  672. assert hasattr(ct.transformers_[0][1], "mean_")
  673. def test_column_transformer_get_feature_names():
  674. X_array = np.array([[0.0, 1.0, 2.0], [2.0, 4.0, 6.0]]).T
  675. ct = ColumnTransformer([("trans", Trans(), [0, 1])])
  676. # raise correct error when not fitted
  677. with pytest.raises(NotFittedError):
  678. ct.get_feature_names_out()
  679. # raise correct error when no feature names are available
  680. ct.fit(X_array)
  681. msg = re.escape(
  682. "Transformer trans (type Trans) does not provide get_feature_names_out"
  683. )
  684. with pytest.raises(AttributeError, match=msg):
  685. ct.get_feature_names_out()
  686. def test_column_transformer_special_strings():
  687. # one 'drop' -> ignore
  688. X_array = np.array([[0.0, 1.0, 2.0], [2.0, 4.0, 6.0]]).T
  689. ct = ColumnTransformer([("trans1", Trans(), [0]), ("trans2", "drop", [1])])
  690. exp = np.array([[0.0], [1.0], [2.0]])
  691. assert_array_equal(ct.fit_transform(X_array), exp)
  692. assert_array_equal(ct.fit(X_array).transform(X_array), exp)
  693. assert len(ct.transformers_) == 2
  694. assert ct.transformers_[-1][0] != "remainder"
  695. # all 'drop' -> return shape 0 array
  696. ct = ColumnTransformer([("trans1", "drop", [0]), ("trans2", "drop", [1])])
  697. assert_array_equal(ct.fit(X_array).transform(X_array).shape, (3, 0))
  698. assert_array_equal(ct.fit_transform(X_array).shape, (3, 0))
  699. assert len(ct.transformers_) == 2
  700. assert ct.transformers_[-1][0] != "remainder"
  701. # 'passthrough'
  702. X_array = np.array([[0.0, 1.0, 2.0], [2.0, 4.0, 6.0]]).T
  703. ct = ColumnTransformer([("trans1", Trans(), [0]), ("trans2", "passthrough", [1])])
  704. exp = X_array
  705. assert_array_equal(ct.fit_transform(X_array), exp)
  706. assert_array_equal(ct.fit(X_array).transform(X_array), exp)
  707. assert len(ct.transformers_) == 2
  708. assert ct.transformers_[-1][0] != "remainder"
  709. def test_column_transformer_remainder():
  710. X_array = np.array([[0, 1, 2], [2, 4, 6]]).T
  711. X_res_first = np.array([0, 1, 2]).reshape(-1, 1)
  712. X_res_second = np.array([2, 4, 6]).reshape(-1, 1)
  713. X_res_both = X_array
  714. # default drop
  715. ct = ColumnTransformer([("trans1", Trans(), [0])])
  716. assert_array_equal(ct.fit_transform(X_array), X_res_first)
  717. assert_array_equal(ct.fit(X_array).transform(X_array), X_res_first)
  718. assert len(ct.transformers_) == 2
  719. assert ct.transformers_[-1][0] == "remainder"
  720. assert ct.transformers_[-1][1] == "drop"
  721. assert_array_equal(ct.transformers_[-1][2], [1])
  722. # specify passthrough
  723. ct = ColumnTransformer([("trans", Trans(), [0])], remainder="passthrough")
  724. assert_array_equal(ct.fit_transform(X_array), X_res_both)
  725. assert_array_equal(ct.fit(X_array).transform(X_array), X_res_both)
  726. assert len(ct.transformers_) == 2
  727. assert ct.transformers_[-1][0] == "remainder"
  728. assert ct.transformers_[-1][1] == "passthrough"
  729. assert_array_equal(ct.transformers_[-1][2], [1])
  730. # column order is not preserved (passed through added to end)
  731. ct = ColumnTransformer([("trans1", Trans(), [1])], remainder="passthrough")
  732. assert_array_equal(ct.fit_transform(X_array), X_res_both[:, ::-1])
  733. assert_array_equal(ct.fit(X_array).transform(X_array), X_res_both[:, ::-1])
  734. assert len(ct.transformers_) == 2
  735. assert ct.transformers_[-1][0] == "remainder"
  736. assert ct.transformers_[-1][1] == "passthrough"
  737. assert_array_equal(ct.transformers_[-1][2], [0])
  738. # passthrough when all actual transformers are skipped
  739. ct = ColumnTransformer([("trans1", "drop", [0])], remainder="passthrough")
  740. assert_array_equal(ct.fit_transform(X_array), X_res_second)
  741. assert_array_equal(ct.fit(X_array).transform(X_array), X_res_second)
  742. assert len(ct.transformers_) == 2
  743. assert ct.transformers_[-1][0] == "remainder"
  744. assert ct.transformers_[-1][1] == "passthrough"
  745. assert_array_equal(ct.transformers_[-1][2], [1])
  746. # check default for make_column_transformer
  747. ct = make_column_transformer((Trans(), [0]))
  748. assert ct.remainder == "drop"
  749. @pytest.mark.parametrize(
  750. "key", [[0], np.array([0]), slice(0, 1), np.array([True, False])]
  751. )
  752. def test_column_transformer_remainder_numpy(key):
  753. # test different ways that columns are specified with passthrough
  754. X_array = np.array([[0, 1, 2], [2, 4, 6]]).T
  755. X_res_both = X_array
  756. ct = ColumnTransformer([("trans1", Trans(), key)], remainder="passthrough")
  757. assert_array_equal(ct.fit_transform(X_array), X_res_both)
  758. assert_array_equal(ct.fit(X_array).transform(X_array), X_res_both)
  759. assert len(ct.transformers_) == 2
  760. assert ct.transformers_[-1][0] == "remainder"
  761. assert ct.transformers_[-1][1] == "passthrough"
  762. assert_array_equal(ct.transformers_[-1][2], [1])
  763. @pytest.mark.parametrize(
  764. "key",
  765. [
  766. [0],
  767. slice(0, 1),
  768. np.array([True, False]),
  769. ["first"],
  770. "pd-index",
  771. np.array(["first"]),
  772. np.array(["first"], dtype=object),
  773. slice(None, "first"),
  774. slice("first", "first"),
  775. ],
  776. )
  777. def test_column_transformer_remainder_pandas(key):
  778. # test different ways that columns are specified with passthrough
  779. pd = pytest.importorskip("pandas")
  780. if isinstance(key, str) and key == "pd-index":
  781. key = pd.Index(["first"])
  782. X_array = np.array([[0, 1, 2], [2, 4, 6]]).T
  783. X_df = pd.DataFrame(X_array, columns=["first", "second"])
  784. X_res_both = X_array
  785. ct = ColumnTransformer([("trans1", Trans(), key)], remainder="passthrough")
  786. assert_array_equal(ct.fit_transform(X_df), X_res_both)
  787. assert_array_equal(ct.fit(X_df).transform(X_df), X_res_both)
  788. assert len(ct.transformers_) == 2
  789. assert ct.transformers_[-1][0] == "remainder"
  790. assert ct.transformers_[-1][1] == "passthrough"
  791. assert_array_equal(ct.transformers_[-1][2], [1])
  792. @pytest.mark.parametrize(
  793. "key", [[0], np.array([0]), slice(0, 1), np.array([True, False, False])]
  794. )
  795. def test_column_transformer_remainder_transformer(key):
  796. X_array = np.array([[0, 1, 2], [2, 4, 6], [8, 6, 4]]).T
  797. X_res_both = X_array.copy()
  798. # second and third columns are doubled when remainder = DoubleTrans
  799. X_res_both[:, 1:3] *= 2
  800. ct = ColumnTransformer([("trans1", Trans(), key)], remainder=DoubleTrans())
  801. assert_array_equal(ct.fit_transform(X_array), X_res_both)
  802. assert_array_equal(ct.fit(X_array).transform(X_array), X_res_both)
  803. assert len(ct.transformers_) == 2
  804. assert ct.transformers_[-1][0] == "remainder"
  805. assert isinstance(ct.transformers_[-1][1], DoubleTrans)
  806. assert_array_equal(ct.transformers_[-1][2], [1, 2])
  807. def test_column_transformer_no_remaining_remainder_transformer():
  808. X_array = np.array([[0, 1, 2], [2, 4, 6], [8, 6, 4]]).T
  809. ct = ColumnTransformer([("trans1", Trans(), [0, 1, 2])], remainder=DoubleTrans())
  810. assert_array_equal(ct.fit_transform(X_array), X_array)
  811. assert_array_equal(ct.fit(X_array).transform(X_array), X_array)
  812. assert len(ct.transformers_) == 1
  813. assert ct.transformers_[-1][0] != "remainder"
  814. def test_column_transformer_drops_all_remainder_transformer():
  815. X_array = np.array([[0, 1, 2], [2, 4, 6], [8, 6, 4]]).T
  816. # columns are doubled when remainder = DoubleTrans
  817. X_res_both = 2 * X_array.copy()[:, 1:3]
  818. ct = ColumnTransformer([("trans1", "drop", [0])], remainder=DoubleTrans())
  819. assert_array_equal(ct.fit_transform(X_array), X_res_both)
  820. assert_array_equal(ct.fit(X_array).transform(X_array), X_res_both)
  821. assert len(ct.transformers_) == 2
  822. assert ct.transformers_[-1][0] == "remainder"
  823. assert isinstance(ct.transformers_[-1][1], DoubleTrans)
  824. assert_array_equal(ct.transformers_[-1][2], [1, 2])
  825. def test_column_transformer_sparse_remainder_transformer():
  826. X_array = np.array([[0, 1, 2], [2, 4, 6], [8, 6, 4]]).T
  827. ct = ColumnTransformer(
  828. [("trans1", Trans(), [0])], remainder=SparseMatrixTrans(), sparse_threshold=0.8
  829. )
  830. X_trans = ct.fit_transform(X_array)
  831. assert sparse.issparse(X_trans)
  832. # SparseMatrixTrans creates 3 features for each column. There is
  833. # one column in ``transformers``, thus:
  834. assert X_trans.shape == (3, 3 + 1)
  835. exp_array = np.hstack((X_array[:, 0].reshape(-1, 1), np.eye(3)))
  836. assert_array_equal(X_trans.toarray(), exp_array)
  837. assert len(ct.transformers_) == 2
  838. assert ct.transformers_[-1][0] == "remainder"
  839. assert isinstance(ct.transformers_[-1][1], SparseMatrixTrans)
  840. assert_array_equal(ct.transformers_[-1][2], [1, 2])
  841. def test_column_transformer_drop_all_sparse_remainder_transformer():
  842. X_array = np.array([[0, 1, 2], [2, 4, 6], [8, 6, 4]]).T
  843. ct = ColumnTransformer(
  844. [("trans1", "drop", [0])], remainder=SparseMatrixTrans(), sparse_threshold=0.8
  845. )
  846. X_trans = ct.fit_transform(X_array)
  847. assert sparse.issparse(X_trans)
  848. # SparseMatrixTrans creates 3 features for each column, thus:
  849. assert X_trans.shape == (3, 3)
  850. assert_array_equal(X_trans.toarray(), np.eye(3))
  851. assert len(ct.transformers_) == 2
  852. assert ct.transformers_[-1][0] == "remainder"
  853. assert isinstance(ct.transformers_[-1][1], SparseMatrixTrans)
  854. assert_array_equal(ct.transformers_[-1][2], [1, 2])
  855. def test_column_transformer_get_set_params_with_remainder():
  856. ct = ColumnTransformer(
  857. [("trans1", StandardScaler(), [0])], remainder=StandardScaler()
  858. )
  859. exp = {
  860. "n_jobs": None,
  861. "remainder": ct.remainder,
  862. "remainder__copy": True,
  863. "remainder__with_mean": True,
  864. "remainder__with_std": True,
  865. "sparse_threshold": 0.3,
  866. "trans1": ct.transformers[0][1],
  867. "trans1__copy": True,
  868. "trans1__with_mean": True,
  869. "trans1__with_std": True,
  870. "transformers": ct.transformers,
  871. "transformer_weights": None,
  872. "verbose_feature_names_out": True,
  873. "verbose": False,
  874. }
  875. assert ct.get_params() == exp
  876. ct.set_params(remainder__with_std=False)
  877. assert not ct.get_params()["remainder__with_std"]
  878. ct.set_params(trans1="passthrough")
  879. exp = {
  880. "n_jobs": None,
  881. "remainder": ct.remainder,
  882. "remainder__copy": True,
  883. "remainder__with_mean": True,
  884. "remainder__with_std": False,
  885. "sparse_threshold": 0.3,
  886. "trans1": "passthrough",
  887. "transformers": ct.transformers,
  888. "transformer_weights": None,
  889. "verbose_feature_names_out": True,
  890. "verbose": False,
  891. }
  892. assert ct.get_params() == exp
  893. def test_column_transformer_no_estimators():
  894. X_array = np.array([[0, 1, 2], [2, 4, 6], [8, 6, 4]]).astype("float").T
  895. ct = ColumnTransformer([], remainder=StandardScaler())
  896. params = ct.get_params()
  897. assert params["remainder__with_mean"]
  898. X_trans = ct.fit_transform(X_array)
  899. assert X_trans.shape == X_array.shape
  900. assert len(ct.transformers_) == 1
  901. assert ct.transformers_[-1][0] == "remainder"
  902. assert ct.transformers_[-1][2] == [0, 1, 2]
  903. @pytest.mark.parametrize(
  904. ["est", "pattern"],
  905. [
  906. (
  907. ColumnTransformer(
  908. [("trans1", Trans(), [0]), ("trans2", Trans(), [1])],
  909. remainder=DoubleTrans(),
  910. ),
  911. (
  912. r"\[ColumnTransformer\].*\(1 of 3\) Processing trans1.* total=.*\n"
  913. r"\[ColumnTransformer\].*\(2 of 3\) Processing trans2.* total=.*\n"
  914. r"\[ColumnTransformer\].*\(3 of 3\) Processing remainder.* total=.*\n$"
  915. ),
  916. ),
  917. (
  918. ColumnTransformer(
  919. [("trans1", Trans(), [0]), ("trans2", Trans(), [1])],
  920. remainder="passthrough",
  921. ),
  922. (
  923. r"\[ColumnTransformer\].*\(1 of 3\) Processing trans1.* total=.*\n"
  924. r"\[ColumnTransformer\].*\(2 of 3\) Processing trans2.* total=.*\n"
  925. r"\[ColumnTransformer\].*\(3 of 3\) Processing remainder.* total=.*\n$"
  926. ),
  927. ),
  928. (
  929. ColumnTransformer(
  930. [("trans1", Trans(), [0]), ("trans2", "drop", [1])],
  931. remainder="passthrough",
  932. ),
  933. (
  934. r"\[ColumnTransformer\].*\(1 of 2\) Processing trans1.* total=.*\n"
  935. r"\[ColumnTransformer\].*\(2 of 2\) Processing remainder.* total=.*\n$"
  936. ),
  937. ),
  938. (
  939. ColumnTransformer(
  940. [("trans1", Trans(), [0]), ("trans2", "passthrough", [1])],
  941. remainder="passthrough",
  942. ),
  943. (
  944. r"\[ColumnTransformer\].*\(1 of 3\) Processing trans1.* total=.*\n"
  945. r"\[ColumnTransformer\].*\(2 of 3\) Processing trans2.* total=.*\n"
  946. r"\[ColumnTransformer\].*\(3 of 3\) Processing remainder.* total=.*\n$"
  947. ),
  948. ),
  949. (
  950. ColumnTransformer([("trans1", Trans(), [0])], remainder="passthrough"),
  951. (
  952. r"\[ColumnTransformer\].*\(1 of 2\) Processing trans1.* total=.*\n"
  953. r"\[ColumnTransformer\].*\(2 of 2\) Processing remainder.* total=.*\n$"
  954. ),
  955. ),
  956. (
  957. ColumnTransformer(
  958. [("trans1", Trans(), [0]), ("trans2", Trans(), [1])], remainder="drop"
  959. ),
  960. (
  961. r"\[ColumnTransformer\].*\(1 of 2\) Processing trans1.* total=.*\n"
  962. r"\[ColumnTransformer\].*\(2 of 2\) Processing trans2.* total=.*\n$"
  963. ),
  964. ),
  965. (
  966. ColumnTransformer([("trans1", Trans(), [0])], remainder="drop"),
  967. r"\[ColumnTransformer\].*\(1 of 1\) Processing trans1.* total=.*\n$",
  968. ),
  969. ],
  970. )
  971. @pytest.mark.parametrize("method", ["fit", "fit_transform"])
  972. def test_column_transformer_verbose(est, pattern, method, capsys):
  973. X_array = np.array([[0, 1, 2], [2, 4, 6], [8, 6, 4]]).T
  974. func = getattr(est, method)
  975. est.set_params(verbose=False)
  976. func(X_array)
  977. assert not capsys.readouterr().out, "Got output for verbose=False"
  978. est.set_params(verbose=True)
  979. func(X_array)
  980. assert re.match(pattern, capsys.readouterr()[0])
  981. def test_column_transformer_no_estimators_set_params():
  982. ct = ColumnTransformer([]).set_params(n_jobs=2)
  983. assert ct.n_jobs == 2
  984. def test_column_transformer_callable_specifier():
  985. # assert that function gets the full array
  986. X_array = np.array([[0, 1, 2], [2, 4, 6]]).T
  987. X_res_first = np.array([[0, 1, 2]]).T
  988. def func(X):
  989. assert_array_equal(X, X_array)
  990. return [0]
  991. ct = ColumnTransformer([("trans", Trans(), func)], remainder="drop")
  992. assert_array_equal(ct.fit_transform(X_array), X_res_first)
  993. assert_array_equal(ct.fit(X_array).transform(X_array), X_res_first)
  994. assert callable(ct.transformers[0][2])
  995. assert ct.transformers_[0][2] == [0]
  996. def test_column_transformer_callable_specifier_dataframe():
  997. # assert that function gets the full dataframe
  998. pd = pytest.importorskip("pandas")
  999. X_array = np.array([[0, 1, 2], [2, 4, 6]]).T
  1000. X_res_first = np.array([[0, 1, 2]]).T
  1001. X_df = pd.DataFrame(X_array, columns=["first", "second"])
  1002. def func(X):
  1003. assert_array_equal(X.columns, X_df.columns)
  1004. assert_array_equal(X.values, X_df.values)
  1005. return ["first"]
  1006. ct = ColumnTransformer([("trans", Trans(), func)], remainder="drop")
  1007. assert_array_equal(ct.fit_transform(X_df), X_res_first)
  1008. assert_array_equal(ct.fit(X_df).transform(X_df), X_res_first)
  1009. assert callable(ct.transformers[0][2])
  1010. assert ct.transformers_[0][2] == ["first"]
  1011. def test_column_transformer_negative_column_indexes():
  1012. X = np.random.randn(2, 2)
  1013. X_categories = np.array([[1], [2]])
  1014. X = np.concatenate([X, X_categories], axis=1)
  1015. ohe = OneHotEncoder()
  1016. tf_1 = ColumnTransformer([("ohe", ohe, [-1])], remainder="passthrough")
  1017. tf_2 = ColumnTransformer([("ohe", ohe, [2])], remainder="passthrough")
  1018. assert_array_equal(tf_1.fit_transform(X), tf_2.fit_transform(X))
  1019. @pytest.mark.parametrize("array_type", [np.asarray, sparse.csr_matrix])
  1020. def test_column_transformer_mask_indexing(array_type):
  1021. # Regression test for #14510
  1022. # Boolean array-like does not behave as boolean array with sparse matrices.
  1023. X = np.transpose([[1, 2, 3], [4, 5, 6], [5, 6, 7], [8, 9, 10]])
  1024. X = array_type(X)
  1025. column_transformer = ColumnTransformer(
  1026. [("identity", FunctionTransformer(), [False, True, False, True])]
  1027. )
  1028. X_trans = column_transformer.fit_transform(X)
  1029. assert X_trans.shape == (3, 2)
  1030. def test_n_features_in():
  1031. # make sure n_features_in is what is passed as input to the column
  1032. # transformer.
  1033. X = [[1, 2], [3, 4], [5, 6]]
  1034. ct = ColumnTransformer([("a", DoubleTrans(), [0]), ("b", DoubleTrans(), [1])])
  1035. assert not hasattr(ct, "n_features_in_")
  1036. ct.fit(X)
  1037. assert ct.n_features_in_ == 2
  1038. @pytest.mark.parametrize(
  1039. "cols, pattern, include, exclude",
  1040. [
  1041. (["col_int", "col_float"], None, np.number, None),
  1042. (["col_int", "col_float"], None, None, object),
  1043. (["col_int", "col_float"], None, [int, float], None),
  1044. (["col_str"], None, [object], None),
  1045. (["col_str"], None, object, None),
  1046. (["col_float"], None, float, None),
  1047. (["col_float"], "at$", [np.number], None),
  1048. (["col_int"], None, [int], None),
  1049. (["col_int"], "^col_int", [np.number], None),
  1050. (["col_float", "col_str"], "float|str", None, None),
  1051. (["col_str"], "^col_s", None, [int]),
  1052. ([], "str$", float, None),
  1053. (["col_int", "col_float", "col_str"], None, [np.number, object], None),
  1054. ],
  1055. )
  1056. def test_make_column_selector_with_select_dtypes(cols, pattern, include, exclude):
  1057. pd = pytest.importorskip("pandas")
  1058. X_df = pd.DataFrame(
  1059. {
  1060. "col_int": np.array([0, 1, 2], dtype=int),
  1061. "col_float": np.array([0.0, 1.0, 2.0], dtype=float),
  1062. "col_str": ["one", "two", "three"],
  1063. },
  1064. columns=["col_int", "col_float", "col_str"],
  1065. )
  1066. selector = make_column_selector(
  1067. dtype_include=include, dtype_exclude=exclude, pattern=pattern
  1068. )
  1069. assert_array_equal(selector(X_df), cols)
  1070. def test_column_transformer_with_make_column_selector():
  1071. # Functional test for column transformer + column selector
  1072. pd = pytest.importorskip("pandas")
  1073. X_df = pd.DataFrame(
  1074. {
  1075. "col_int": np.array([0, 1, 2], dtype=int),
  1076. "col_float": np.array([0.0, 1.0, 2.0], dtype=float),
  1077. "col_cat": ["one", "two", "one"],
  1078. "col_str": ["low", "middle", "high"],
  1079. },
  1080. columns=["col_int", "col_float", "col_cat", "col_str"],
  1081. )
  1082. X_df["col_str"] = X_df["col_str"].astype("category")
  1083. cat_selector = make_column_selector(dtype_include=["category", object])
  1084. num_selector = make_column_selector(dtype_include=np.number)
  1085. ohe = OneHotEncoder()
  1086. scaler = StandardScaler()
  1087. ct_selector = make_column_transformer((ohe, cat_selector), (scaler, num_selector))
  1088. ct_direct = make_column_transformer(
  1089. (ohe, ["col_cat", "col_str"]), (scaler, ["col_float", "col_int"])
  1090. )
  1091. X_selector = ct_selector.fit_transform(X_df)
  1092. X_direct = ct_direct.fit_transform(X_df)
  1093. assert_allclose(X_selector, X_direct)
  1094. def test_make_column_selector_error():
  1095. selector = make_column_selector(dtype_include=np.number)
  1096. X = np.array([[0.1, 0.2]])
  1097. msg = "make_column_selector can only be applied to pandas dataframes"
  1098. with pytest.raises(ValueError, match=msg):
  1099. selector(X)
  1100. def test_make_column_selector_pickle():
  1101. pd = pytest.importorskip("pandas")
  1102. X_df = pd.DataFrame(
  1103. {
  1104. "col_int": np.array([0, 1, 2], dtype=int),
  1105. "col_float": np.array([0.0, 1.0, 2.0], dtype=float),
  1106. "col_str": ["one", "two", "three"],
  1107. },
  1108. columns=["col_int", "col_float", "col_str"],
  1109. )
  1110. selector = make_column_selector(dtype_include=[object])
  1111. selector_picked = pickle.loads(pickle.dumps(selector))
  1112. assert_array_equal(selector(X_df), selector_picked(X_df))
  1113. @pytest.mark.parametrize(
  1114. "empty_col",
  1115. [[], np.array([], dtype=int), lambda x: []],
  1116. ids=["list", "array", "callable"],
  1117. )
  1118. def test_feature_names_empty_columns(empty_col):
  1119. pd = pytest.importorskip("pandas")
  1120. df = pd.DataFrame({"col1": ["a", "a", "b"], "col2": ["z", "z", "z"]})
  1121. ct = ColumnTransformer(
  1122. transformers=[
  1123. ("ohe", OneHotEncoder(), ["col1", "col2"]),
  1124. ("empty_features", OneHotEncoder(), empty_col),
  1125. ],
  1126. )
  1127. ct.fit(df)
  1128. assert_array_equal(
  1129. ct.get_feature_names_out(), ["ohe__col1_a", "ohe__col1_b", "ohe__col2_z"]
  1130. )
  1131. @pytest.mark.parametrize(
  1132. "selector",
  1133. [
  1134. [1],
  1135. lambda x: [1],
  1136. ["col2"],
  1137. lambda x: ["col2"],
  1138. [False, True],
  1139. lambda x: [False, True],
  1140. ],
  1141. )
  1142. def test_feature_names_out_pandas(selector):
  1143. """Checks name when selecting only the second column"""
  1144. pd = pytest.importorskip("pandas")
  1145. df = pd.DataFrame({"col1": ["a", "a", "b"], "col2": ["z", "z", "z"]})
  1146. ct = ColumnTransformer([("ohe", OneHotEncoder(), selector)])
  1147. ct.fit(df)
  1148. assert_array_equal(ct.get_feature_names_out(), ["ohe__col2_z"])
  1149. @pytest.mark.parametrize(
  1150. "selector", [[1], lambda x: [1], [False, True], lambda x: [False, True]]
  1151. )
  1152. def test_feature_names_out_non_pandas(selector):
  1153. """Checks name when selecting the second column with numpy array"""
  1154. X = [["a", "z"], ["a", "z"], ["b", "z"]]
  1155. ct = ColumnTransformer([("ohe", OneHotEncoder(), selector)])
  1156. ct.fit(X)
  1157. assert_array_equal(ct.get_feature_names_out(), ["ohe__x1_z"])
  1158. @pytest.mark.parametrize("remainder", ["passthrough", StandardScaler()])
  1159. def test_sk_visual_block_remainder(remainder):
  1160. # remainder='passthrough' or an estimator will be shown in repr_html
  1161. ohe = OneHotEncoder()
  1162. ct = ColumnTransformer(
  1163. transformers=[("ohe", ohe, ["col1", "col2"])], remainder=remainder
  1164. )
  1165. visual_block = ct._sk_visual_block_()
  1166. assert visual_block.names == ("ohe", "remainder")
  1167. assert visual_block.name_details == (["col1", "col2"], "")
  1168. assert visual_block.estimators == (ohe, remainder)
  1169. def test_sk_visual_block_remainder_drop():
  1170. # remainder='drop' is not shown in repr_html
  1171. ohe = OneHotEncoder()
  1172. ct = ColumnTransformer(transformers=[("ohe", ohe, ["col1", "col2"])])
  1173. visual_block = ct._sk_visual_block_()
  1174. assert visual_block.names == ("ohe",)
  1175. assert visual_block.name_details == (["col1", "col2"],)
  1176. assert visual_block.estimators == (ohe,)
  1177. @pytest.mark.parametrize("remainder", ["passthrough", StandardScaler()])
  1178. def test_sk_visual_block_remainder_fitted_pandas(remainder):
  1179. # Remainder shows the columns after fitting
  1180. pd = pytest.importorskip("pandas")
  1181. ohe = OneHotEncoder()
  1182. ct = ColumnTransformer(
  1183. transformers=[("ohe", ohe, ["col1", "col2"])], remainder=remainder
  1184. )
  1185. df = pd.DataFrame(
  1186. {
  1187. "col1": ["a", "b", "c"],
  1188. "col2": ["z", "z", "z"],
  1189. "col3": [1, 2, 3],
  1190. "col4": [3, 4, 5],
  1191. }
  1192. )
  1193. ct.fit(df)
  1194. visual_block = ct._sk_visual_block_()
  1195. assert visual_block.names == ("ohe", "remainder")
  1196. assert visual_block.name_details == (["col1", "col2"], ["col3", "col4"])
  1197. assert visual_block.estimators == (ohe, remainder)
  1198. @pytest.mark.parametrize("remainder", ["passthrough", StandardScaler()])
  1199. def test_sk_visual_block_remainder_fitted_numpy(remainder):
  1200. # Remainder shows the indices after fitting
  1201. X = np.array([[1, 2, 3], [4, 5, 6]], dtype=float)
  1202. scaler = StandardScaler()
  1203. ct = ColumnTransformer(
  1204. transformers=[("scale", scaler, [0, 2])], remainder=remainder
  1205. )
  1206. ct.fit(X)
  1207. visual_block = ct._sk_visual_block_()
  1208. assert visual_block.names == ("scale", "remainder")
  1209. assert visual_block.name_details == ([0, 2], [1])
  1210. assert visual_block.estimators == (scaler, remainder)
  1211. @pytest.mark.parametrize("explicit_colname", ["first", "second", 0, 1])
  1212. @pytest.mark.parametrize("remainder", [Trans(), "passthrough", "drop"])
  1213. def test_column_transformer_reordered_column_names_remainder(
  1214. explicit_colname, remainder
  1215. ):
  1216. """Test the interaction between remainder and column transformer"""
  1217. pd = pytest.importorskip("pandas")
  1218. X_fit_array = np.array([[0, 1, 2], [2, 4, 6]]).T
  1219. X_fit_df = pd.DataFrame(X_fit_array, columns=["first", "second"])
  1220. X_trans_array = np.array([[2, 4, 6], [0, 1, 2]]).T
  1221. X_trans_df = pd.DataFrame(X_trans_array, columns=["second", "first"])
  1222. tf = ColumnTransformer([("bycol", Trans(), explicit_colname)], remainder=remainder)
  1223. tf.fit(X_fit_df)
  1224. X_fit_trans = tf.transform(X_fit_df)
  1225. # Changing the order still works
  1226. X_trans = tf.transform(X_trans_df)
  1227. assert_allclose(X_trans, X_fit_trans)
  1228. # extra columns are ignored
  1229. X_extended_df = X_fit_df.copy()
  1230. X_extended_df["third"] = [3, 6, 9]
  1231. X_trans = tf.transform(X_extended_df)
  1232. assert_allclose(X_trans, X_fit_trans)
  1233. if isinstance(explicit_colname, str):
  1234. # Raise error if columns are specified by names but input only allows
  1235. # to specify by position, e.g. numpy array instead of a pandas df.
  1236. X_array = X_fit_array.copy()
  1237. err_msg = "Specifying the columns"
  1238. with pytest.raises(ValueError, match=err_msg):
  1239. tf.transform(X_array)
  1240. def test_feature_name_validation_missing_columns_drop_passthough():
  1241. """Test the interaction between {'drop', 'passthrough'} and
  1242. missing column names."""
  1243. pd = pytest.importorskip("pandas")
  1244. X = np.ones(shape=(3, 4))
  1245. df = pd.DataFrame(X, columns=["a", "b", "c", "d"])
  1246. df_dropped = df.drop("c", axis=1)
  1247. # with remainder='passthrough', all columns seen during `fit` must be
  1248. # present
  1249. tf = ColumnTransformer([("bycol", Trans(), [1])], remainder="passthrough")
  1250. tf.fit(df)
  1251. msg = r"columns are missing: {'c'}"
  1252. with pytest.raises(ValueError, match=msg):
  1253. tf.transform(df_dropped)
  1254. # with remainder='drop', it is allowed to have column 'c' missing
  1255. tf = ColumnTransformer([("bycol", Trans(), [1])], remainder="drop")
  1256. tf.fit(df)
  1257. df_dropped_trans = tf.transform(df_dropped)
  1258. df_fit_trans = tf.transform(df)
  1259. assert_allclose(df_dropped_trans, df_fit_trans)
  1260. # bycol drops 'c', thus it is allowed for 'c' to be missing
  1261. tf = ColumnTransformer([("bycol", "drop", ["c"])], remainder="passthrough")
  1262. tf.fit(df)
  1263. df_dropped_trans = tf.transform(df_dropped)
  1264. df_fit_trans = tf.transform(df)
  1265. assert_allclose(df_dropped_trans, df_fit_trans)
  1266. def test_feature_names_in_():
  1267. """Feature names are stored in column transformer.
  1268. Column transformer deliberately does not check for column name consistency.
  1269. It only checks that the non-dropped names seen in `fit` are seen
  1270. in `transform`. This behavior is already tested in
  1271. `test_feature_name_validation_missing_columns_drop_passthough`"""
  1272. pd = pytest.importorskip("pandas")
  1273. feature_names = ["a", "c", "d"]
  1274. df = pd.DataFrame([[1, 2, 3]], columns=feature_names)
  1275. ct = ColumnTransformer([("bycol", Trans(), ["a", "d"])], remainder="passthrough")
  1276. ct.fit(df)
  1277. assert_array_equal(ct.feature_names_in_, feature_names)
  1278. assert isinstance(ct.feature_names_in_, np.ndarray)
  1279. assert ct.feature_names_in_.dtype == object
  1280. class TransWithNames(Trans):
  1281. def __init__(self, feature_names_out=None):
  1282. self.feature_names_out = feature_names_out
  1283. def get_feature_names_out(self, input_features=None):
  1284. if self.feature_names_out is not None:
  1285. return np.asarray(self.feature_names_out, dtype=object)
  1286. return input_features
  1287. @pytest.mark.parametrize(
  1288. "transformers, remainder, expected_names",
  1289. [
  1290. (
  1291. [
  1292. ("bycol1", TransWithNames(), ["d", "c"]),
  1293. ("bycol2", "passthrough", ["d"]),
  1294. ],
  1295. "passthrough",
  1296. ["bycol1__d", "bycol1__c", "bycol2__d", "remainder__a", "remainder__b"],
  1297. ),
  1298. (
  1299. [
  1300. ("bycol1", TransWithNames(), ["d", "c"]),
  1301. ("bycol2", "passthrough", ["d"]),
  1302. ],
  1303. "drop",
  1304. ["bycol1__d", "bycol1__c", "bycol2__d"],
  1305. ),
  1306. (
  1307. [
  1308. ("bycol1", TransWithNames(), ["b"]),
  1309. ("bycol2", "drop", ["d"]),
  1310. ],
  1311. "passthrough",
  1312. ["bycol1__b", "remainder__a", "remainder__c"],
  1313. ),
  1314. (
  1315. [
  1316. ("bycol1", TransWithNames(["pca1", "pca2"]), ["a", "b", "d"]),
  1317. ],
  1318. "passthrough",
  1319. ["bycol1__pca1", "bycol1__pca2", "remainder__c"],
  1320. ),
  1321. (
  1322. [
  1323. ("bycol1", TransWithNames(["a", "b"]), ["d"]),
  1324. ("bycol2", "passthrough", ["b"]),
  1325. ],
  1326. "drop",
  1327. ["bycol1__a", "bycol1__b", "bycol2__b"],
  1328. ),
  1329. (
  1330. [
  1331. ("bycol1", TransWithNames([f"pca{i}" for i in range(2)]), ["b"]),
  1332. ("bycol2", TransWithNames([f"pca{i}" for i in range(2)]), ["b"]),
  1333. ],
  1334. "passthrough",
  1335. [
  1336. "bycol1__pca0",
  1337. "bycol1__pca1",
  1338. "bycol2__pca0",
  1339. "bycol2__pca1",
  1340. "remainder__a",
  1341. "remainder__c",
  1342. "remainder__d",
  1343. ],
  1344. ),
  1345. (
  1346. [
  1347. ("bycol1", "drop", ["d"]),
  1348. ],
  1349. "drop",
  1350. [],
  1351. ),
  1352. (
  1353. [
  1354. ("bycol1", TransWithNames(), slice(1, 3)),
  1355. ],
  1356. "drop",
  1357. ["bycol1__b", "bycol1__c"],
  1358. ),
  1359. (
  1360. [
  1361. ("bycol1", TransWithNames(), ["b"]),
  1362. ("bycol2", "drop", slice(3, 4)),
  1363. ],
  1364. "passthrough",
  1365. ["bycol1__b", "remainder__a", "remainder__c"],
  1366. ),
  1367. (
  1368. [
  1369. ("bycol1", TransWithNames(), ["d", "c"]),
  1370. ("bycol2", "passthrough", slice(3, 4)),
  1371. ],
  1372. "passthrough",
  1373. ["bycol1__d", "bycol1__c", "bycol2__d", "remainder__a", "remainder__b"],
  1374. ),
  1375. (
  1376. [
  1377. ("bycol1", TransWithNames(), slice("b", "c")),
  1378. ],
  1379. "drop",
  1380. ["bycol1__b", "bycol1__c"],
  1381. ),
  1382. (
  1383. [
  1384. ("bycol1", TransWithNames(), ["b"]),
  1385. ("bycol2", "drop", slice("c", "d")),
  1386. ],
  1387. "passthrough",
  1388. ["bycol1__b", "remainder__a"],
  1389. ),
  1390. (
  1391. [
  1392. ("bycol1", TransWithNames(), ["d", "c"]),
  1393. ("bycol2", "passthrough", slice("c", "d")),
  1394. ],
  1395. "passthrough",
  1396. [
  1397. "bycol1__d",
  1398. "bycol1__c",
  1399. "bycol2__c",
  1400. "bycol2__d",
  1401. "remainder__a",
  1402. "remainder__b",
  1403. ],
  1404. ),
  1405. ],
  1406. )
  1407. def test_verbose_feature_names_out_true(transformers, remainder, expected_names):
  1408. """Check feature_names_out for verbose_feature_names_out=True (default)"""
  1409. pd = pytest.importorskip("pandas")
  1410. df = pd.DataFrame([[1, 2, 3, 4]], columns=["a", "b", "c", "d"])
  1411. ct = ColumnTransformer(
  1412. transformers,
  1413. remainder=remainder,
  1414. )
  1415. ct.fit(df)
  1416. names = ct.get_feature_names_out()
  1417. assert isinstance(names, np.ndarray)
  1418. assert names.dtype == object
  1419. assert_array_equal(names, expected_names)
  1420. @pytest.mark.parametrize(
  1421. "transformers, remainder, expected_names",
  1422. [
  1423. (
  1424. [
  1425. ("bycol1", TransWithNames(), ["d", "c"]),
  1426. ("bycol2", "passthrough", ["a"]),
  1427. ],
  1428. "passthrough",
  1429. ["d", "c", "a", "b"],
  1430. ),
  1431. (
  1432. [
  1433. ("bycol1", TransWithNames(["a"]), ["d", "c"]),
  1434. ("bycol2", "passthrough", ["d"]),
  1435. ],
  1436. "drop",
  1437. ["a", "d"],
  1438. ),
  1439. (
  1440. [
  1441. ("bycol1", TransWithNames(), ["b"]),
  1442. ("bycol2", "drop", ["d"]),
  1443. ],
  1444. "passthrough",
  1445. ["b", "a", "c"],
  1446. ),
  1447. (
  1448. [
  1449. ("bycol1", TransWithNames(["pca1", "pca2"]), ["a", "b", "d"]),
  1450. ],
  1451. "passthrough",
  1452. ["pca1", "pca2", "c"],
  1453. ),
  1454. (
  1455. [
  1456. ("bycol1", TransWithNames(["a", "c"]), ["d"]),
  1457. ("bycol2", "passthrough", ["d"]),
  1458. ],
  1459. "drop",
  1460. ["a", "c", "d"],
  1461. ),
  1462. (
  1463. [
  1464. ("bycol1", TransWithNames([f"pca{i}" for i in range(2)]), ["b"]),
  1465. ("bycol2", TransWithNames([f"kpca{i}" for i in range(2)]), ["b"]),
  1466. ],
  1467. "passthrough",
  1468. ["pca0", "pca1", "kpca0", "kpca1", "a", "c", "d"],
  1469. ),
  1470. (
  1471. [
  1472. ("bycol1", "drop", ["d"]),
  1473. ],
  1474. "drop",
  1475. [],
  1476. ),
  1477. (
  1478. [
  1479. ("bycol1", TransWithNames(), slice(1, 2)),
  1480. ("bycol2", "drop", ["d"]),
  1481. ],
  1482. "passthrough",
  1483. ["b", "a", "c"],
  1484. ),
  1485. (
  1486. [
  1487. ("bycol1", TransWithNames(), ["b"]),
  1488. ("bycol2", "drop", slice(3, 4)),
  1489. ],
  1490. "passthrough",
  1491. ["b", "a", "c"],
  1492. ),
  1493. (
  1494. [
  1495. ("bycol1", TransWithNames(), ["d", "c"]),
  1496. ("bycol2", "passthrough", slice(0, 2)),
  1497. ],
  1498. "drop",
  1499. ["d", "c", "a", "b"],
  1500. ),
  1501. (
  1502. [
  1503. ("bycol1", TransWithNames(), slice("a", "b")),
  1504. ("bycol2", "drop", ["d"]),
  1505. ],
  1506. "passthrough",
  1507. ["a", "b", "c"],
  1508. ),
  1509. (
  1510. [
  1511. ("bycol1", TransWithNames(), ["b"]),
  1512. ("bycol2", "drop", slice("c", "d")),
  1513. ],
  1514. "passthrough",
  1515. ["b", "a"],
  1516. ),
  1517. (
  1518. [
  1519. ("bycol1", TransWithNames(), ["d", "c"]),
  1520. ("bycol2", "passthrough", slice("a", "b")),
  1521. ],
  1522. "drop",
  1523. ["d", "c", "a", "b"],
  1524. ),
  1525. (
  1526. [
  1527. ("bycol1", TransWithNames(), ["d", "c"]),
  1528. ("bycol2", "passthrough", slice("b", "b")),
  1529. ],
  1530. "drop",
  1531. ["d", "c", "b"],
  1532. ),
  1533. ],
  1534. )
  1535. def test_verbose_feature_names_out_false(transformers, remainder, expected_names):
  1536. """Check feature_names_out for verbose_feature_names_out=False"""
  1537. pd = pytest.importorskip("pandas")
  1538. df = pd.DataFrame([[1, 2, 3, 4]], columns=["a", "b", "c", "d"])
  1539. ct = ColumnTransformer(
  1540. transformers,
  1541. remainder=remainder,
  1542. verbose_feature_names_out=False,
  1543. )
  1544. ct.fit(df)
  1545. names = ct.get_feature_names_out()
  1546. assert isinstance(names, np.ndarray)
  1547. assert names.dtype == object
  1548. assert_array_equal(names, expected_names)
  1549. @pytest.mark.parametrize(
  1550. "transformers, remainder, colliding_columns",
  1551. [
  1552. (
  1553. [
  1554. ("bycol1", TransWithNames(), ["b"]),
  1555. ("bycol2", "passthrough", ["b"]),
  1556. ],
  1557. "drop",
  1558. "['b']",
  1559. ),
  1560. (
  1561. [
  1562. ("bycol1", TransWithNames(["c", "d"]), ["c"]),
  1563. ("bycol2", "passthrough", ["c"]),
  1564. ],
  1565. "drop",
  1566. "['c']",
  1567. ),
  1568. (
  1569. [
  1570. ("bycol1", TransWithNames(["a"]), ["b"]),
  1571. ("bycol2", "passthrough", ["b"]),
  1572. ],
  1573. "passthrough",
  1574. "['a']",
  1575. ),
  1576. (
  1577. [
  1578. ("bycol1", TransWithNames(["a"]), ["b"]),
  1579. ("bycol2", "drop", ["b"]),
  1580. ],
  1581. "passthrough",
  1582. "['a']",
  1583. ),
  1584. (
  1585. [
  1586. ("bycol1", TransWithNames(["c", "b"]), ["b"]),
  1587. ("bycol2", "passthrough", ["c", "b"]),
  1588. ],
  1589. "drop",
  1590. "['b', 'c']",
  1591. ),
  1592. (
  1593. [
  1594. ("bycol1", TransWithNames(["a"]), ["b"]),
  1595. ("bycol2", "passthrough", ["a"]),
  1596. ("bycol3", TransWithNames(["a"]), ["b"]),
  1597. ],
  1598. "passthrough",
  1599. "['a']",
  1600. ),
  1601. (
  1602. [
  1603. ("bycol1", TransWithNames(["a", "b"]), ["b"]),
  1604. ("bycol2", "passthrough", ["a"]),
  1605. ("bycol3", TransWithNames(["b"]), ["c"]),
  1606. ],
  1607. "passthrough",
  1608. "['a', 'b']",
  1609. ),
  1610. (
  1611. [
  1612. ("bycol1", TransWithNames([f"pca{i}" for i in range(6)]), ["b"]),
  1613. ("bycol2", TransWithNames([f"pca{i}" for i in range(6)]), ["b"]),
  1614. ],
  1615. "passthrough",
  1616. "['pca0', 'pca1', 'pca2', 'pca3', 'pca4', ...]",
  1617. ),
  1618. (
  1619. [
  1620. ("bycol1", TransWithNames(["a", "b"]), slice(1, 2)),
  1621. ("bycol2", "passthrough", ["a"]),
  1622. ("bycol3", TransWithNames(["b"]), ["c"]),
  1623. ],
  1624. "passthrough",
  1625. "['a', 'b']",
  1626. ),
  1627. (
  1628. [
  1629. ("bycol1", TransWithNames(["a", "b"]), ["b"]),
  1630. ("bycol2", "passthrough", slice(0, 1)),
  1631. ("bycol3", TransWithNames(["b"]), ["c"]),
  1632. ],
  1633. "passthrough",
  1634. "['a', 'b']",
  1635. ),
  1636. (
  1637. [
  1638. ("bycol1", TransWithNames(["a", "b"]), slice("b", "c")),
  1639. ("bycol2", "passthrough", ["a"]),
  1640. ("bycol3", TransWithNames(["b"]), ["c"]),
  1641. ],
  1642. "passthrough",
  1643. "['a', 'b']",
  1644. ),
  1645. (
  1646. [
  1647. ("bycol1", TransWithNames(["a", "b"]), ["b"]),
  1648. ("bycol2", "passthrough", slice("a", "a")),
  1649. ("bycol3", TransWithNames(["b"]), ["c"]),
  1650. ],
  1651. "passthrough",
  1652. "['a', 'b']",
  1653. ),
  1654. ],
  1655. )
  1656. def test_verbose_feature_names_out_false_errors(
  1657. transformers, remainder, colliding_columns
  1658. ):
  1659. """Check feature_names_out for verbose_feature_names_out=False"""
  1660. pd = pytest.importorskip("pandas")
  1661. df = pd.DataFrame([[1, 2, 3, 4]], columns=["a", "b", "c", "d"])
  1662. ct = ColumnTransformer(
  1663. transformers,
  1664. remainder=remainder,
  1665. verbose_feature_names_out=False,
  1666. )
  1667. ct.fit(df)
  1668. msg = re.escape(
  1669. f"Output feature names: {colliding_columns} are not unique. Please set "
  1670. "verbose_feature_names_out=True to add prefixes to feature names"
  1671. )
  1672. with pytest.raises(ValueError, match=msg):
  1673. ct.get_feature_names_out()
  1674. @pytest.mark.parametrize("verbose_feature_names_out", [True, False])
  1675. @pytest.mark.parametrize("remainder", ["drop", "passthrough"])
  1676. def test_column_transformer_set_output(verbose_feature_names_out, remainder):
  1677. """Check column transformer behavior with set_output."""
  1678. pd = pytest.importorskip("pandas")
  1679. df = pd.DataFrame([[1, 2, 3, 4]], columns=["a", "b", "c", "d"], index=[10])
  1680. ct = ColumnTransformer(
  1681. [("first", TransWithNames(), ["a", "c"]), ("second", TransWithNames(), ["d"])],
  1682. remainder=remainder,
  1683. verbose_feature_names_out=verbose_feature_names_out,
  1684. )
  1685. X_trans = ct.fit_transform(df)
  1686. assert isinstance(X_trans, np.ndarray)
  1687. ct.set_output(transform="pandas")
  1688. df_test = pd.DataFrame([[1, 2, 3, 4]], columns=df.columns, index=[20])
  1689. X_trans = ct.transform(df_test)
  1690. assert isinstance(X_trans, pd.DataFrame)
  1691. feature_names_out = ct.get_feature_names_out()
  1692. assert_array_equal(X_trans.columns, feature_names_out)
  1693. assert_array_equal(X_trans.index, df_test.index)
  1694. @pytest.mark.parametrize("remainder", ["drop", "passthrough"])
  1695. @pytest.mark.parametrize("fit_transform", [True, False])
  1696. def test_column_transform_set_output_mixed(remainder, fit_transform):
  1697. """Check ColumnTransformer outputs mixed types correctly."""
  1698. pd = pytest.importorskip("pandas")
  1699. df = pd.DataFrame(
  1700. {
  1701. "pet": pd.Series(["dog", "cat", "snake"], dtype="category"),
  1702. "color": pd.Series(["green", "blue", "red"], dtype="object"),
  1703. "age": [1.4, 2.1, 4.4],
  1704. "height": [20, 40, 10],
  1705. "distance": pd.Series([20, pd.NA, 100], dtype="Int32"),
  1706. }
  1707. )
  1708. ct = ColumnTransformer(
  1709. [
  1710. (
  1711. "color_encode",
  1712. OneHotEncoder(sparse_output=False, dtype="int8"),
  1713. ["color"],
  1714. ),
  1715. ("age", StandardScaler(), ["age"]),
  1716. ],
  1717. remainder=remainder,
  1718. verbose_feature_names_out=False,
  1719. ).set_output(transform="pandas")
  1720. if fit_transform:
  1721. X_trans = ct.fit_transform(df)
  1722. else:
  1723. X_trans = ct.fit(df).transform(df)
  1724. assert isinstance(X_trans, pd.DataFrame)
  1725. assert_array_equal(X_trans.columns, ct.get_feature_names_out())
  1726. expected_dtypes = {
  1727. "color_blue": "int8",
  1728. "color_green": "int8",
  1729. "color_red": "int8",
  1730. "age": "float64",
  1731. "pet": "category",
  1732. "height": "int64",
  1733. "distance": "Int32",
  1734. }
  1735. for col, dtype in X_trans.dtypes.items():
  1736. assert dtype == expected_dtypes[col]
  1737. @pytest.mark.parametrize("remainder", ["drop", "passthrough"])
  1738. def test_column_transform_set_output_after_fitting(remainder):
  1739. pd = pytest.importorskip("pandas")
  1740. df = pd.DataFrame(
  1741. {
  1742. "pet": pd.Series(["dog", "cat", "snake"], dtype="category"),
  1743. "age": [1.4, 2.1, 4.4],
  1744. "height": [20, 40, 10],
  1745. }
  1746. )
  1747. ct = ColumnTransformer(
  1748. [
  1749. (
  1750. "color_encode",
  1751. OneHotEncoder(sparse_output=False, dtype="int16"),
  1752. ["pet"],
  1753. ),
  1754. ("age", StandardScaler(), ["age"]),
  1755. ],
  1756. remainder=remainder,
  1757. verbose_feature_names_out=False,
  1758. )
  1759. # fit without calling set_output
  1760. X_trans = ct.fit_transform(df)
  1761. assert isinstance(X_trans, np.ndarray)
  1762. assert X_trans.dtype == "float64"
  1763. ct.set_output(transform="pandas")
  1764. X_trans_df = ct.transform(df)
  1765. expected_dtypes = {
  1766. "pet_cat": "int16",
  1767. "pet_dog": "int16",
  1768. "pet_snake": "int16",
  1769. "height": "int64",
  1770. "age": "float64",
  1771. }
  1772. for col, dtype in X_trans_df.dtypes.items():
  1773. assert dtype == expected_dtypes[col]
  1774. # PandasOutTransformer that does not define get_feature_names_out and always expects
  1775. # the input to be a DataFrame.
  1776. class PandasOutTransformer(BaseEstimator):
  1777. def __init__(self, offset=1.0):
  1778. self.offset = offset
  1779. def fit(self, X, y=None):
  1780. pd = pytest.importorskip("pandas")
  1781. assert isinstance(X, pd.DataFrame)
  1782. return self
  1783. def transform(self, X, y=None):
  1784. pd = pytest.importorskip("pandas")
  1785. assert isinstance(X, pd.DataFrame)
  1786. return X - self.offset
  1787. def set_output(self, transform=None):
  1788. # This transformer will always output a DataFrame regardless of the
  1789. # configuration.
  1790. return self
  1791. @pytest.mark.parametrize(
  1792. "trans_1, expected_verbose_names, expected_non_verbose_names",
  1793. [
  1794. (
  1795. PandasOutTransformer(offset=2.0),
  1796. ["trans_0__feat1", "trans_1__feat0"],
  1797. ["feat1", "feat0"],
  1798. ),
  1799. (
  1800. "drop",
  1801. ["trans_0__feat1"],
  1802. ["feat1"],
  1803. ),
  1804. (
  1805. "passthrough",
  1806. ["trans_0__feat1", "trans_1__feat0"],
  1807. ["feat1", "feat0"],
  1808. ),
  1809. ],
  1810. )
  1811. def test_transformers_with_pandas_out_but_not_feature_names_out(
  1812. trans_1, expected_verbose_names, expected_non_verbose_names
  1813. ):
  1814. """Check that set_config(transform="pandas") is compatible with more transformers.
  1815. Specifically, if transformers returns a DataFrame, but does not define
  1816. `get_feature_names_out`.
  1817. """
  1818. pd = pytest.importorskip("pandas")
  1819. X_df = pd.DataFrame({"feat0": [1.0, 2.0, 3.0], "feat1": [2.0, 3.0, 4.0]})
  1820. ct = ColumnTransformer(
  1821. [
  1822. ("trans_0", PandasOutTransformer(offset=3.0), ["feat1"]),
  1823. ("trans_1", trans_1, ["feat0"]),
  1824. ]
  1825. )
  1826. X_trans_np = ct.fit_transform(X_df)
  1827. assert isinstance(X_trans_np, np.ndarray)
  1828. # `ct` does not have `get_feature_names_out` because `PandasOutTransformer` does
  1829. # not define the method.
  1830. with pytest.raises(AttributeError, match="not provide get_feature_names_out"):
  1831. ct.get_feature_names_out()
  1832. # The feature names are prefixed because verbose_feature_names_out=True is default
  1833. ct.set_output(transform="pandas")
  1834. X_trans_df0 = ct.fit_transform(X_df)
  1835. assert_array_equal(X_trans_df0.columns, expected_verbose_names)
  1836. ct.set_params(verbose_feature_names_out=False)
  1837. X_trans_df1 = ct.fit_transform(X_df)
  1838. assert_array_equal(X_trans_df1.columns, expected_non_verbose_names)
  1839. @pytest.mark.parametrize(
  1840. "empty_selection",
  1841. [[], np.array([False, False]), [False, False]],
  1842. ids=["list", "bool", "bool_int"],
  1843. )
  1844. def test_empty_selection_pandas_output(empty_selection):
  1845. """Check that pandas output works when there is an empty selection.
  1846. Non-regression test for gh-25487
  1847. """
  1848. pd = pytest.importorskip("pandas")
  1849. X = pd.DataFrame([[1.0, 2.2], [3.0, 1.0]], columns=["a", "b"])
  1850. ct = ColumnTransformer(
  1851. [
  1852. ("categorical", "passthrough", empty_selection),
  1853. ("numerical", StandardScaler(), ["a", "b"]),
  1854. ],
  1855. verbose_feature_names_out=True,
  1856. )
  1857. ct.set_output(transform="pandas")
  1858. X_out = ct.fit_transform(X)
  1859. assert_array_equal(X_out.columns, ["numerical__a", "numerical__b"])
  1860. ct.set_params(verbose_feature_names_out=False)
  1861. X_out = ct.fit_transform(X)
  1862. assert_array_equal(X_out.columns, ["a", "b"])
  1863. def test_raise_error_if_index_not_aligned():
  1864. """Check column transformer raises error if indices are not aligned.
  1865. Non-regression test for gh-26210.
  1866. """
  1867. pd = pytest.importorskip("pandas")
  1868. X = pd.DataFrame([[1.0, 2.2], [3.0, 1.0]], columns=["a", "b"], index=[8, 3])
  1869. reset_index_transformer = FunctionTransformer(
  1870. lambda x: x.reset_index(drop=True), feature_names_out="one-to-one"
  1871. )
  1872. ct = ColumnTransformer(
  1873. [
  1874. ("num1", "passthrough", ["a"]),
  1875. ("num2", reset_index_transformer, ["b"]),
  1876. ],
  1877. )
  1878. ct.set_output(transform="pandas")
  1879. msg = (
  1880. "Concatenating DataFrames from the transformer's output lead to"
  1881. " an inconsistent number of samples. The output may have Pandas"
  1882. " Indexes that do not match."
  1883. )
  1884. with pytest.raises(ValueError, match=msg):
  1885. ct.fit_transform(X)
  1886. def test_remainder_set_output():
  1887. """Check that the output is set for the remainder.
  1888. Non-regression test for #26306.
  1889. """
  1890. pd = pytest.importorskip("pandas")
  1891. df = pd.DataFrame({"a": [True, False, True], "b": [1, 2, 3]})
  1892. ct = make_column_transformer(
  1893. (VarianceThreshold(), make_column_selector(dtype_include=bool)),
  1894. remainder=VarianceThreshold(),
  1895. verbose_feature_names_out=False,
  1896. )
  1897. ct.set_output(transform="pandas")
  1898. out = ct.fit_transform(df)
  1899. pd.testing.assert_frame_equal(out, df)
  1900. ct.set_output(transform="default")
  1901. out = ct.fit_transform(df)
  1902. assert isinstance(out, np.ndarray)