##// END OF EJS Templates
return vectors instead of structured arrays when 1d and no names
Jonathan Taylor -
Show More
@@ -1,422 +1,431 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 ======
4 4 Rmagic
5 5 ======
6 6
7 7 Magic command interface for interactive work with R via rpy2
8 8
9 9 Usage
10 10 =====
11 11
12 12 ``%R``
13 13
14 14 {R_DOC}
15 15
16 16 ``%Rpush``
17 17
18 18 {RPUSH_DOC}
19 19
20 20 ``%Rpull``
21 21
22 22 {RPULL_DOC}
23 23
24 24 """
25 25
26 26 #-----------------------------------------------------------------------------
27 27 # Copyright (C) 2012 The IPython Development Team
28 28 #
29 29 # Distributed under the terms of the BSD License. The full license is in
30 30 # the file COPYING, distributed as part of this software.
31 31 #-----------------------------------------------------------------------------
32 32
33 33 import sys
34 34 import tempfile
35 35 from glob import glob
36 36 from shutil import rmtree
37 37 from getopt import getopt
38 38
39 39 # numpy and rpy2 imports
40 40
41 41 import numpy as np
42 42
43 43 import rpy2.rinterface as ri
44 44 import rpy2.robjects as ro
45 45 from rpy2.robjects.numpy2ri import numpy2ri
46 46 ro.conversion.py2ri = numpy2ri
47 47
48 48 # IPython imports
49 49
50 50 from IPython.core.displaypub import publish_display_data
51 51 from IPython.core.magic import (Magics, magics_class, cell_magic, line_magic,
52 52 line_cell_magic)
53 53 from IPython.testing.skipdoctest import skip_doctest
54 54 from IPython.core.magic_arguments import (
55 55 argument, magic_arguments, parse_argstring
56 56 )
57 57 from IPython.utils.py3compat import str_to_unicode, unicode_to_str
58 58
59 59 class RMagicError(ri.RRuntimeError):
60 60 pass
61 61
62 62 def Rconverter(Robj):
63 63 """
64 64 Convert an object in R's namespace to one suitable
65 65 for ipython's namespace.
66 66
67 67 For a data.frame, it tries to return a structured array.
68 68
69 69 Parameters
70 70 ----------
71 71
72 72 Robj: an R object returned from rpy2
73 73 """
74 74 if is_data_frame(Robj):
75 dimRobj = dimR(Robj)
76 if dimRobj != ri.NULL:
77 dimRobj = list(np.array(dimRobj))
78 if len(dimRobj) > 1:
79 try:
75 80 names = np.array(Robj.do_slot('names'))
76 81 Robj = np.rec.fromarrays(Robj, names = tuple(names))
82 except LookupError:
83 pass
77 84 return np.asarray(Robj)
78 85
79 86 is_data_frame = None
87 dimR = None
80 88
81 89 @magics_class
82 90 class RMagics(Magics):
83 91 """A set of magics useful for interactive work with R via rpy2.
84 92 """
85 93
86 94 def __init__(self, shell, Rconverter=Rconverter,
87 95 pyconverter=np.asarray,
88 96 cache_display_data=False):
89 97 """
90 98 Parameters
91 99 ----------
92 100
93 101 shell : IPython shell
94 102
95 103 pyconverter : callable
96 104 To be called on values in ipython namespace before
97 105 assigning to variables in rpy2.
98 106
99 107 cache_display_data : bool
100 108 If True, the published results of the final call to R are
101 109 cached in the variable 'display_cache'.
102 110
103 111 """
104 112 super(RMagics, self).__init__(shell)
105 113 self.cache_display_data = cache_display_data
106 114
107 115 self.r = ro.R()
108 global is_data_frame
116 global is_data_frame, dimR
109 117 is_data_frame = self.r('is.data.frame')
118 dimR = self.r('dim')
110 119
111 120 self.Rstdout_cache = []
112 121 self.pyconverter = pyconverter
113 122 self.Rconverter = Rconverter
114 123
115 124 def eval(self, line):
116 125 '''
117 126 Parse and evaluate a line with rpy2.
118 127 Returns the output to R's stdout() connection
119 128 and the value of eval(parse(line)).
120 129 '''
121 130 old_writeconsole = ri.get_writeconsole()
122 131 ri.set_writeconsole(self.write_console)
123 132 try:
124 133 value = ri.baseenv['eval'](ri.parse(line))
125 134 except (ri.RRuntimeError, ValueError) as exception:
126 135 raise RMagicError(unicode_to_str('parsing and evaluating line "%s". R traceback: "%s"\n' %
127 136 (line, str_to_unicode(exception.message, 'utf-8'))))
128 137 text_output = self.flush()
129 138 ri.set_writeconsole(old_writeconsole)
130 139 return text_output, value
131 140
132 141 def write_console(self, output):
133 142 '''
134 143 A hook to capture R's stdout in a cache.
135 144 '''
136 145 self.Rstdout_cache.append(output)
137 146
138 147 def flush(self):
139 148 '''
140 149 Flush R's stdout cache to a string, returning the string.
141 150 '''
142 151 value = ''.join([str_to_unicode(s, 'utf-8') for s in self.Rstdout_cache])
143 152 self.Rstdout_cache = []
144 153 return value
145 154
146 155 @skip_doctest
147 156 @line_magic
148 157 def Rpush(self, line):
149 158 '''
150 159 A line-level magic for R that pushes
151 160 variables from python to rpy2. The line should be made up
152 161 of whitespace separated variable names in the IPython
153 162 namespace::
154 163
155 164 In [7]: import numpy as np
156 165
157 166 In [8]: X = np.array([4.5,6.3,7.9])
158 167
159 168 In [9]: X.mean()
160 169 Out[9]: 6.2333333333333343
161 170
162 171 In [10]: %Rpush X
163 172
164 173 In [11]: %R mean(X)
165 174 Out[11]: array([ 6.23333333])
166 175
167 176 '''
168 177
169 178 inputs = line.split(' ')
170 179 for input in inputs:
171 180 self.r.assign(input, self.pyconverter(self.shell.user_ns[input]))
172 181
173 182 @skip_doctest
174 183 @line_magic
175 184 def Rpull(self, line):
176 185 '''
177 186 A line-level magic for R that pulls
178 187 variables from python to rpy2::
179 188
180 189 In [18]: _ = %R x = c(3,4,6.7); y = c(4,6,7); z = c('a',3,4)
181 190
182 191 In [19]: %Rpull x y z
183 192
184 193 In [20]: x
185 194 Out[20]: array([ 3. , 4. , 6.7])
186 195
187 196 In [21]: y
188 197 Out[21]: array([ 4., 6., 7.])
189 198
190 199 In [22]: z
191 200 Out[22]:
192 201 array(['a', '3', '4'],
193 202 dtype='|S1')
194 203
195 204
196 205 Notes
197 206 -----
198 207
199 208 Beware that R names can have '.' so this is not fool proof.
200 209 To avoid this, don't name your R objects with '.'s...
201 210
202 211 '''
203 212 outputs = line.split(' ')
204 213 for output in outputs:
205 214 self.shell.push({output:self.Rconverter(self.r(output))})
206 215
207 216
208 217 @skip_doctest
209 218 @magic_arguments()
210 219 @argument(
211 220 '-i', '--input', action='append',
212 221 help='Names of input variable from shell.user_ns to be assigned to R variables of the same names after calling self.pyconverter. Multiple names can be passed separated only by commas with no whitespace.'
213 222 )
214 223 @argument(
215 224 '-o', '--output', action='append',
216 225 help='Names of variables to be pushed from rpy2 to shell.user_ns after executing cell body and applying self.Rconverter. Multiple names can be passed separated only by commas with no whitespace.'
217 226 )
218 227 @argument(
219 228 '-w', '--width', type=int,
220 229 help='Width of png plotting device sent as an argument to *png* in R.'
221 230 )
222 231 @argument(
223 232 '-h', '--height', type=int,
224 233 help='Height of png plotting device sent as an argument to *png* in R.'
225 234 )
226 235
227 236 @argument(
228 237 '-u', '--units', type=int,
229 238 help='Units of png plotting device sent as an argument to *png* in R. One of ["px", "in", "cm", "mm"].'
230 239 )
231 240 @argument(
232 241 '-p', '--pointsize', type=int,
233 242 help='Pointsize of png plotting device sent as an argument to *png* in R.'
234 243 )
235 244 @argument(
236 245 '-b', '--bg',
237 246 help='Background of png plotting device sent as an argument to *png* in R.'
238 247 )
239 248 @argument(
240 249 '-n', '--noreturn',
241 250 help='Force the magic to not return anything.',
242 251 action='store_true',
243 252 default=False
244 253 )
245 254 @argument(
246 255 'code',
247 256 nargs='*',
248 257 )
249 258 @line_cell_magic
250 259 def R(self, line, cell=None):
251 260 '''
252 261 Execute code in R, and pull some of the results back into the Python namespace.
253 262
254 263 In line mode, this will evaluate an expression and convert the returned value to a Python object.
255 264 The return value is determined by rpy2's behaviour of returning the result of evaluating the
256 265 final line. Multiple R lines can be executed by joining them with semicolons::
257 266
258 267 In [9]: %R X=c(1,4,5,7); sd(X); mean(X)
259 268 Out[9]: array([ 4.25])
260 269
261 270 As a cell, this will run a block of R code, without bringing anything back by default::
262 271
263 272 In [10]: %%R
264 273 ....: Y = c(2,4,3,9)
265 274 ....: print(summary(lm(Y~X)))
266 275 ....:
267 276
268 277 Call:
269 278 lm(formula = Y ~ X)
270 279
271 280 Residuals:
272 281 1 2 3 4
273 282 0.88 -0.24 -2.28 1.64
274 283
275 284 Coefficients:
276 285 Estimate Std. Error t value Pr(>|t|)
277 286 (Intercept) 0.0800 2.3000 0.035 0.975
278 287 X 1.0400 0.4822 2.157 0.164
279 288
280 289 Residual standard error: 2.088 on 2 degrees of freedom
281 290 Multiple R-squared: 0.6993,Adjusted R-squared: 0.549
282 291 F-statistic: 4.651 on 1 and 2 DF, p-value: 0.1638
283 292
284 293 In the notebook, plots are published as the output of the cell.
285 294
286 295 %R plot(X, Y)
287 296
288 297 will create a scatter plot of X bs Y.
289 298
290 299 If cell is not None and line has some R code, it is prepended to
291 300 the R code in cell.
292 301
293 302 Objects can be passed back and forth between rpy2 and python via the -i -o flags in line::
294 303
295 304 In [14]: Z = np.array([1,4,5,10])
296 305
297 306 In [15]: %R -i Z mean(Z)
298 307 Out[15]: array([ 5.])
299 308
300 309
301 310 In [16]: %R -o W W=Z*mean(Z)
302 311 Out[16]: array([ 5., 20., 25., 50.])
303 312
304 313 In [17]: W
305 314 Out[17]: array([ 5., 20., 25., 50.])
306 315
307 316 The return value is determined by these rules:
308 317
309 318 * If the cell is not None, the magic returns None.
310 319
311 320 * If the cell evaluates as False, the resulting value is returned
312 321 unless the final line prints something to the console, in
313 322 which case None is returned.
314 323
315 324 * If the final line results in a NULL value when evaluated
316 325 by rpy2, then None is returned.
317 326
318 327
319 328 '''
320 329
321 330 args = parse_argstring(self.R, line)
322 331
323 332 # arguments 'code' in line are prepended to
324 333 # the cell lines
325 334 if not cell:
326 335 code = ''
327 336 return_output = True
328 337 line_mode = True
329 338 else:
330 339 code = cell
331 340 return_output = False
332 341 line_mode = False
333 342
334 343 code = ' '.join(args.code) + code
335 344
336 345 if args.input:
337 346 for input in ','.join(args.input).split(','):
338 347 self.r.assign(input, self.pyconverter(self.shell.user_ns[input]))
339 348
340 349 png_argdict = dict([(n, getattr(args, n)) for n in ['units', 'height', 'width', 'bg', 'pointsize']])
341 350 png_args = ','.join(['%s=%s' % (o,v) for o, v in png_argdict.items() if v is not None])
342 351 # execute the R code in a temporary directory
343 352
344 353 tmpd = tempfile.mkdtemp()
345 354 self.r('png("%s/Rplots%%03d.png",%s)' % (tmpd, png_args))
346 355
347 356 text_output = ''
348 357 if line_mode:
349 358 for line in code.split(';'):
350 359 text_result, result = self.eval(line)
351 360 text_output += text_result
352 361 if text_result:
353 362 # the last line printed something to the console so we won't return it
354 363 return_output = False
355 364 else:
356 365 text_result, result = self.eval(code)
357 366 text_output += text_result
358 367
359 368 self.r('dev.off()')
360 369
361 370 # read out all the saved .png files
362 371
363 372 images = [open(imgfile, 'rb').read() for imgfile in glob("%s/Rplots*png" % tmpd)]
364 373
365 374 # now publish the images
366 375 # mimicking IPython/zmq/pylab/backend_inline.py
367 376 fmt = 'png'
368 377 mimetypes = { 'png' : 'image/png', 'svg' : 'image/svg+xml' }
369 378 mime = mimetypes[fmt]
370 379
371 380 # publish the printed R objects, if any
372 381
373 382 display_data = []
374 383 if text_output:
375 384 display_data.append(('RMagic.R', {'text/plain':text_output}))
376 385
377 386 # flush text streams before sending figures, helps a little with output
378 387 for image in images:
379 388 # synchronization in the console (though it's a bandaid, not a real sln)
380 389 sys.stdout.flush(); sys.stderr.flush()
381 390 display_data.append(('RMagic.R', {mime: image}))
382 391
383 392 # kill the temporary directory
384 393 rmtree(tmpd)
385 394
386 395 # try to turn every output into a numpy array
387 396 # this means that output are assumed to be castable
388 397 # as numpy arrays
389 398
390 399 if args.output:
391 400 for output in ','.join(args.output).split(','):
392 401 self.shell.push({output:self.Rconverter(self.r(output))})
393 402
394 403 for tag, disp_d in display_data:
395 404 publish_display_data(tag, disp_d)
396 405
397 406 # this will keep a reference to the display_data
398 407 # which might be useful to other objects who happen to use
399 408 # this method
400 409
401 410 if self.cache_display_data:
402 411 self.display_cache = display_data
403 412
404 413 # if in line mode and return_output, return the result as an ndarray
405 414 if return_output and not args.noreturn:
406 415 if result != ri.NULL:
407 416 return self.Rconverter(result)
408 417
409 418 __doc__ = __doc__.format(
410 419 R_DOC = ' '*8 + RMagics.R.__doc__,
411 420 RPUSH_DOC = ' '*8 + RMagics.Rpush.__doc__,
412 421 RPULL_DOC = ' '*8 + RMagics.Rpull.__doc__
413 422 )
414 423
415 424
416 425 _loaded = False
417 426 def load_ipython_extension(ip):
418 427 """Load the extension in IPython."""
419 428 global _loaded
420 429 if not _loaded:
421 430 ip.register_magics(RMagics)
422 431 _loaded = True
@@ -1,41 +1,62 b''
1 1 import numpy as np
2 2 from IPython.core.interactiveshell import InteractiveShell
3 3 from IPython.extensions import rmagic
4 import nose.tools as nt
4 5
5 6 ip = get_ipython()
6 7 ip.magic('load_ext rmagic')
7 8
8 9
9 10 def test_push():
10 11 rm = rmagic.RMagics(ip)
11 12 ip.push({'X':np.arange(5), 'Y':np.array([3,5,4,6,7])})
12 13 ip.run_line_magic('Rpush', 'X Y')
13 14 np.testing.assert_almost_equal(np.asarray(rm.r('X')), ip.user_ns['X'])
14 15 np.testing.assert_almost_equal(np.asarray(rm.r('Y')), ip.user_ns['Y'])
15 16
16 17 def test_pull():
17 18 rm = rmagic.RMagics(ip)
18 19 rm.r('Z=c(11:20)')
19 20 ip.run_line_magic('Rpull', 'Z')
20 21 np.testing.assert_almost_equal(np.asarray(rm.r('Z')), ip.user_ns['Z'])
21 22 np.testing.assert_almost_equal(ip.user_ns['Z'], np.arange(11,21))
22 23
23 # def test_inline():
24 # rm = rmagic.RMagics(ip)
25 # c = ip.run_line_magic('Rinline', 'lm(Y~X)$coef')
26 # np.testing.assert_almost_equal(c, [3.2, 0.9])
24 def test_Rconverter():
25 datapy= np.array([(1, 2.9, 'a'), (2, 3.5, 'b'), (3, 2.1, 'c')],
26 dtype=[('x', '<i4'), ('y', '<f8'), ('z', '|S1')])
27 ip.user_ns['datapy'] = datapy
28 ip.run_line_magic('Rpush', 'datapy')
29
30 # test to see if a copy is being made
31 v = ip.run_line_magic('R', 'datapy')
32 w = ip.run_line_magic('R', 'datapy')
33 np.testing.assert_almost_equal(w['x'], v['x'])
34 np.testing.assert_almost_equal(w['y'], v['y'])
35 nt.assert_true(np.all(w['z'] == v['z']))
36 np.testing.assert_equal(id(w.data), id(v.data))
37 nt.assert_equal(w.dtype, v.dtype)
38
39 ip.run_cell_magic('R', ' -o datar datar=datapy', '')
40
41 u = ip.run_line_magic('R', 'datar')
42 np.testing.assert_almost_equal(u['x'], v['x'])
43 np.testing.assert_almost_equal(u['y'], v['y'])
44 nt.assert_true(np.all(u['z'] == v['z']))
45 np.testing.assert_equal(id(u.data), id(v.data))
46 nt.assert_equal(u.dtype, v.dtype)
47
27 48
28 49 def test_cell_magic():
29 50
30 51 ip.push({'x':np.arange(5), 'y':np.array([3,5,4,6,7])})
31 52 snippet = '''
32 53 print(summary(a))
33 plot(X, Y, pch=23, bg='orange', cex=2)
34 plot(Y, X)
35 print(summary(X))
54 plot(x, y, pch=23, bg='orange', cex=2)
55 plot(x, x)
56 print(summary(x))
36 57 r = resid(a)
37 58 xc = coef(a)
38 59 '''
39 60 ip.run_cell_magic('R', '-i x,y -o r,xc a=lm(y~x)', snippet)
40 61 np.testing.assert_almost_equal(ip.user_ns['xc'], [3.2, 0.9])
41 62 np.testing.assert_almost_equal(ip.user_ns['r'], np.array([-0.2, 0.9, -1. , 0.1, 0.2]))
General Comments 0
You need to be logged in to leave comments. Login now