Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1# -*- coding: utf-8 -*- 

2""" 

3@file 

4@brief Implémente un *learner* qui suit la même API que tout :epkg:`scikit-learn` learner. 

5""" 

6import numpy 

7import pandas 

8from sklearn.base import clone 

9from mlinsights.sklapi import SkBaseLearner, SkLearnParameters 

10 

11 

12class SkBaseLearnerCategory(SkBaseLearner): 

13 

14 """ 

15 Base d'un *learner* qui apprend un learner pour chaque 

16 modalité d'une classe. 

17 

18 Notebooks associés à ce *learner* : 

19 

20 .. runpython:: 

21 :rst: 

22 

23 from papierstat.datasets.documentation import list_notebooks_rst_links 

24 links = list_notebooks_rst_links('lectures', 'wines_color_linear') 

25 links = [' * %s' % s for s in links] 

26 print('\\n'.join(links)) 

27 """ 

28 

29 def __init__(self, colnameind=None, model=None, **kwargs): 

30 """ 

31 Stocke les paramètres dans une classe 

32 @see cl SkLearnParameters, elle garde une copie des 

33 paramètres pour implémenter facilement *get_params* 

34 et ainsi cloner un modèle. 

35 

36 @param colnameind indice ou nom de la colonne qui 

37 contient les modalités de la catégorie 

38 @param model model à appliquer sur chaque catégorie 

39 """ 

40 if not isinstance(colnameind, (int, str)): 

41 raise TypeError( 

42 "colnameind must be str or int not {0}".format(type(colnameind))) 

43 if model is None: 

44 raise ValueError("model must not be None") 

45 kwargs['colnameind'] = colnameind 

46 SkBaseLearner.__init__(self, **kwargs) 

47 self.model = model 

48 self._estimator_type = self.model._estimator_type 

49 

50 @property 

51 def colnameind(self): 

52 """ 

53 Retourne le nom ou l'indice de la catégorie. 

54 """ 

55 return self.P.colnameind 

56 

57 @property 

58 def Models(self): 

59 """ 

60 Retourne les models. 

61 """ 

62 if hasattr(self, 'models'): 

63 return self.models 

64 else: 

65 raise RuntimeError('No trained models') 

66 

67 def _get_cat(self, X): 

68 """ 

69 Retourne les catégories indiquées par *colnameind*. 

70 """ 

71 if isinstance(self.colnameind, str): 

72 if not hasattr(X, 'columns'): 

73 raise TypeError("colnameind='{0}' and X is not a DataFrame but {1}".format( 

74 self.colnameind, type(X))) 

75 return X[self.colnameind] 

76 else: 

77 return X[:, self.colnameind] 

78 

79 def _filter_cat(self, c, X, y=None, sample_weight=None): 

80 """ 

81 Retoure *X*, *y*, *sample_weight* pour la categorie *c* uniquement. 

82 """ 

83 indices = numpy.arange(0, X.shape[0]) 

84 if isinstance(self.colnameind, str): 

85 if not hasattr(X, 'columns'): 

86 raise TypeError("colnameind='{0}' and X is not a DataFrame but {1}".format( 

87 self.colnameind, type(X))) 

88 ind = X[self.colnameind] == c 

89 sa = None if sample_weight is None else sample_weight[ind] 

90 y = None if y is None else y[ind] 

91 ind, x = indices[ind], X.drop(self.colnameind, axis=1)[ind] 

92 elif hasattr(X, 'iloc'): 

93 ind = X[self.colnameind] == c 

94 sa = None if sample_weight is None else sample_weight[ind] 

95 y = None if y is None else y[ind] 

96 ind, x = indices[ind], X.iloc[ind, -self.colnameind] 

97 else: 

98 ind = X[self.colnameind] == c 

99 sa = None if sample_weight is None else sample_weight[ind] 

100 y = None if y is None else y[ind] 

101 ind, x = indices[ind], X[ind, -self.colnameind] 

102 if y is not None and x.shape[0] != y.shape[0]: 

103 raise RuntimeError("Input arrays have different shapes for value='{0}': {1} != {2} (expected: {3}) type(X)={4}".format( 

104 c, X.shape[0], y.shape[0], ind.shape, type(X))) 

105 if sa is not None and x.shape[0] != sa.shape[0]: 

106 raise RuntimeError("Input arrays have different shapes for value='{0}': {1} != {2} (expected: {3}) type(X)={4}".format( 

107 c, X.shape[0], sa.shape[0], ind.shape, type(X))) 

108 return ind, x, y, sa 

109 

110 ################### 

111 # API scikit-learn 

112 ################### 

113 

114 def fit(self, X, y=None, **kwargs): 

115 """ 

116 Apprends un modèle pour chaque modalité d'une catégorie. 

117 

118 @param X features 

119 @param y cibles 

120 @return self, lui-même 

121 

122 La fonction n'est pas parallélisée mais elle le pourrait. 

123 """ 

124 cats = set(self._get_cat(X)) 

125 for c in cats: 

126 if not isinstance(c, str) and numpy.isnan(c): 

127 raise ValueError("One of the row has a missing category.") 

128 

129 sample_weight = kwargs.get('sample_weight', None) 

130 res = {} 

131 for c in sorted(cats): 

132 _, xcat, ycat, scat = self._filter_cat(c, X, y, sample_weight) 

133 mod = clone(self.model) 

134 if scat is not None: 

135 kwargs['sample_weight'] = scat 

136 mod.fit(xcat, ycat, **kwargs) 

137 res[c] = mod 

138 self.models = res 

139 return self 

140 

141 def _any_predict(self, X, fct, *args): 

142 """ 

143 Prédit en appelant le modèle associé à chaque catégorie. 

144 

145 @param X features 

146 @return prédictions 

147 

148 La fonction n'est pas parallélisée mais elle le pourrait. 

149 """ 

150 cats = set(self._get_cat(X)) 

151 for c in cats: 

152 if not isinstance(c, str) and numpy.isnan(c): 

153 raise NotImplementedError( 

154 "No default value is implemented in case of missing value.") 

155 

156 res = [] 

157 for c in sorted(cats): 

158 ind, xcat, ycat, _ = self._filter_cat(c, X, *args) 

159 mod = self.models[c] 

160 meth = getattr(mod, fct) 

161 if ycat is None: 

162 pred = meth(xcat) 

163 else: 

164 pred = meth(xcat, ycat) 

165 if len(pred.shape) == 1: 

166 pred = pred[:, numpy.newaxis] 

167 if len(ind.shape) == 1: 

168 ind = ind[:, numpy.newaxis] 

169 pred = numpy.hstack([pred, ind]) 

170 res.append(pred) 

171 try: 

172 final = numpy.vstack(res) 

173 except ValueError: 

174 # Only one dimension. 

175 final = numpy.hstack(res) 

176 df = pandas.DataFrame(final) 

177 df = df.sort_values( 

178 df.columns[-1]).reset_index(drop=True) # pylint: disable=E1136 

179 df = df.iloc[:, :-1].values 

180 if len(df.shape) == 2 and df.shape[1] == 1: 

181 df = df.ravel() 

182 return df 

183 

184 def predict(self, X): 

185 """ 

186 Prédit en appelant le modèle associé à chaque catégorie. 

187 

188 @param X features 

189 @return prédictions 

190 

191 La fonction n'est pas parallélisée mais elle le pourrait. 

192 """ 

193 return self._any_predict(X, 'predict') 

194 

195 def decision_function(self, X): 

196 """ 

197 Output of the model in case of a regressor, matrix with a score for each class and each sample 

198 for a classifier 

199 

200 @param X Samples, {array-like, sparse matrix}, shape = (n_samples, n_features) 

201 @return array, shape = (n_samples,.), Returns predicted values. 

202 """ 

203 if hasattr(self.model, 'decision_function'): 

204 return self._any_predict(X, 'decision_function') 

205 else: 

206 raise NotImplementedError( 

207 "No decision_function for {0}".format(self.model)) 

208 

209 def predict_proba(self, X): 

210 """ 

211 Output of the model in case of a regressor, matrix with a score for each class and each sample 

212 for a classifier 

213 

214 @param X Samples, {array-like, sparse matrix}, shape = (n_samples, n_features) 

215 @return array, shape = (n_samples,.), Returns predicted values. 

216 """ 

217 if hasattr(self.model, 'predict_proba'): 

218 return self._any_predict(X, 'predict_proba') 

219 else: 

220 raise NotImplementedError( 

221 "No method predict_proba for {0}".format(self.model)) 

222 

223 def score(self, X, y=None, sample_weight=None): 

224 """ 

225 Returns the mean accuracy on the given test data and labels. 

226 

227 @param X Training data, numpy array or sparse matrix of shape [n_samples,n_features] 

228 @param y Target values, numpy array of shape [n_samples, n_targets] (optional) 

229 @param sample_weight Weight values, numpy array of shape [n_samples, n_targets] (optional) 

230 @return score : float, Mean accuracy of self.predict(X) wrt. y. 

231 """ 

232 if self._estimator_type == 'classifier': 

233 from sklearn.metrics import accuracy_score 

234 return accuracy_score(y, self.predict(X), sample_weight=sample_weight) 

235 elif self._estimator_type == 'regressor': 

236 from sklearn.metrics import r2_score 

237 return r2_score(y, self.predict(X), sample_weight=sample_weight) 

238 else: 

239 raise RuntimeError("Unexpected estimator type '{0}', cannot guess default scoring metric.".format( 

240 self._estimator_type)) 

241 

242 ############## 

243 # cloning API 

244 ############## 

245 

246 def get_params(self, deep=True): 

247 """ 

248 Retourne les paramètres qui définissent l'objet 

249 (tous ceux nécessaires pour le cloner). 

250 

251 @param deep unused here 

252 @return dict 

253 """ 

254 res = self.P.to_dict() 

255 res['model'] = self.model 

256 if deep: 

257 p = self.model.get_params(deep) 

258 ps = {'model__{0}'.format( 

259 name): value for name, value in p.items()} 

260 res.update(ps) 

261 return res 

262 

263 def set_params(self, **values): 

264 """ 

265 Change les paramètres qui définissent l'objet 

266 (tous ceux nécessaires pour le cloner). 

267 

268 @param values values 

269 @return dict 

270 """ 

271 if 'model' in values: 

272 self.model = values['model'] 

273 del values['model'] 

274 elif not hasattr(self, 'model') or self.model is None: 

275 raise KeyError("Missing key '{0}' in [{1}]".format( 

276 'model', ', '.join(sorted(values)))) 

277 prefix = 'model__' 

278 ext = {k[len(prefix):]: v for k, v in values.items() 

279 if k.startswith(prefix)} 

280 self.model.set_params(**ext) 

281 existing = self.P.to_dict() 

282 ext = {k: v for k, v in values.items() if not k.startswith(prefix)} 

283 if ext: 

284 existing.update(ext) 

285 self.P = SkLearnParameters(**existing) 

286 return self 

287 

288 ################# 

289 # common methods 

290 ################# 

291 

292 def __repr__(self): 

293 """ 

294 usual 

295 """ 

296 return "{0}({2},{1})".format(self.__class__.__name__, repr(self.P), repr(self.model))