Notas sobre OOP

Anuncio
Programación orientada a objetos
Ocaña Rebull, Jordi
Sánchez Pla, Alex
8 de novembre de 2007
1
Planteamiento y motivación
En este capı́tulo se presentan algunos conceptos básicos de programación orientada a objetos (OOP) en R, tomando en consideración solamente el modelo de
objetos de R (y S) S4, no el antiguo modelo de objetos conocido habitualmente
como S3. Para ello se empieza planteando un problema estadı́stico relacionado
con los modelos mixtos y su implementación mediante funciones “estándar” y se
muestra cómo, a medida que la situación se complica, la OOP podrı́a ayudar a
disponer de una solución elegante y extensible.
1.1
Generación de un modelo lineal mixto simple
Supongamos que para realizar un estudio de simulación es preciso generar conjuntos de datos a partir de un modelo lineal mixto sencillo, del estilo de:
yi = (α0 + Ai ) + (β0 + Bi )x + i
yi = (yi1 , . . . , yim ) representará, en general, un vector de m observaciones de
una variable, posiblemente correlacionadas, obtenidas para el “individuo” (o
“grupo”) i bajo las condiciones x = (x1 , . . . , xm ). Los valores de x representan,
a menudo, instantes sucesivos de tiempo. Dentro de cada individuo se puede
considerar que la trayectoria observada depende linealmente de x, pero la función lineal concreta depende de cada individuo. Esta dependencia se modeliza
mediante unos parámetros de regresión aleatorios (α0 + Ai β0 + Bi ), con una
parte constante, fija, α0 , β0 y una parte que representa una realización de dos
variables aleatorias
Ai ∼ N (0, σα ), Bi ∼ N (0, σβ )
que por simplicidad se considerarán independientes. Finalmente, i = (i1 , . . . , im )
representan también residuos aleatorios,
ij ∼ N (0, σ)
mutuamente independientes e independientes de Ai y de Bi .
Por ejemplo, el código siguiente genera 4 réplicas independientes (individuos)
según el modelo anterior:
1
>
>
>
>
>
>
>
>
>
alfa0 <- 3
beta0 <- 2.5
sAlfa <- 1.5
sBeta <- 2.5
sRes <- 2
nDatos <- 4
x <- 1:10
k <- length(x)
x
[1]
>
>
>
>
>
1
2
3
4
5
6
7
8
9 10
set.seed(127)
a <- alfa0 + rnorm(nDatos, sd = sAlfa)
b <- beta0 + rnorm(nDatos, sd = sBeta)
y <- a + x %o% b + matrix(rnorm(nDatos * k, sd = sRes), ncol = nDatos)
y
[1,]
[2,]
[3,]
[4,]
[5,]
[6,]
[7,]
[8,]
[9,]
[10,]
[,1]
7.827101
11.143801
15.695551
22.412437
24.922213
28.517056
33.833978
41.124718
43.033940
45.787576
[,2]
5.503796
12.431905
15.903439
24.408809
29.557917
33.835794
33.612725
39.176276
43.400738
51.105699
[,3]
7.343257
13.508364
18.914433
22.468927
22.495091
24.086158
32.426765
37.865164
40.938131
44.820739
[,4]
7.982633
4.904760
7.058215
10.802503
15.548872
15.585602
16.861647
19.408568
22.363922
23.300257
Los resultados de la generación se muestran en el gráfico.
> plot(matrix(x, ncol = nDatos, nrow = k), y, type = "o")
2
50
●
●
●
●
●
40
●
●
●
●
30
●
20
y
●
●
●
●
●
●
●
●
●
●
●
●
●
●
10
●
●
●
●
●
●
●
●
●
●
●
●
2
4
6
8
10
matrix(x, ncol = nDatos, nrow = k)
La utilidad general del código anterior puede mejorarse presentándolo en
forma de función:
>
+
+
+
+
+
+
>
>
genMix <- function(n = 1, x, alfaFix = 0, betaFix = 0, sigmaAlfa = 1,
sigmaBeta = 1, sigmaRes = 1) {
a <- alfaFix + rnorm(n, sd = sigmaAlfa)
b <- betaFix + rnorm(n, sd = sigmaBeta)
a + x %o% b + matrix(rnorm(n * length(x), sd = sigmaRes),
ncol = n)
}
set.seed(127)
genMix(nDatos, x, alfa0, beta0, sAlfa, sBeta, sRes)
[1,]
[2,]
[3,]
[4,]
[5,]
[6,]
[7,]
[8,]
[9,]
[10,]
[,1]
7.827101
11.143801
15.695551
22.412437
24.922213
28.517056
33.833978
41.124718
43.033940
45.787576
[,2]
5.503796
12.431905
15.903439
24.408809
29.557917
33.835794
33.612725
39.176276
43.400738
51.105699
[,3]
7.343257
13.508364
18.914433
22.468927
22.495091
24.086158
32.426765
37.865164
40.938131
44.820739
3
[,4]
7.982633
4.904760
7.058215
10.802503
15.548872
15.585602
16.861647
19.408568
22.363922
23.300257
La necesidad de la OOP surge de forma natural al complicar y generalizar el
código, al pretender convertirlo en algo de utilidad general y no algo puramente
de “usar y tirar”.
Supongamos que deseamos generalizar el código anterior permitiendo que los
residuos y/o los factores aleatorios se generen de acuerdo con una distribución
absolutamente continua cualquiera.
Tenemos varias alternativas (a cual peor):
Una posibilidad serı́a crear una función para cada posible combinación de
distribuciones escogidas para ambos efectos aleatorios y para los residuos:
>
+
+
+
+
+
+
>
+
+
+
+
+
+
genMixNormNormCauchy <- function(n = 1, x, alfaFix = 0, betaFix = 0,
sigmaAlfa = 1, sigmaBeta = 1, sigmaRes = 1) {
a <- alfaFix + rnorm(n, sd = sigmaAlfa)
b <- betaFix + rnorm(n, sd = sigmaBeta)
a + x %o% b + matrix(rcauchy(n * length(x), scale = sigmaRes),
ncol = n)
}
genMixNormCauchyCauchy <- function(n = 1, x, alfaFix = 0, betaFix = 0,
sigmaAlfa = 1, sigmaBeta = 1, sigmaRes = 1) {
a <- alfaFix + rnorm(n, sd = sigmaAlfa)
b <- betaFix + rcauchy(n, scale = sigmaBeta)
a + x %o% b + matrix(rcauchy(n * length(x), scale = sigmaRes),
ncol = n)
}
etc...
Algunas distribuciones como la gamma plantean dificultades adicionales, es
necesario centrarlas y reescalarlas adecuadamente para que tengan sentido como
un efecto aleatorio o un residuo:
> genMixNormNormGamma <- function(n = 1, x, alfaFix = 0, betaFix = 0,
+
sigmaAlfa = 1, sigmaBeta = 1, sigmaRes = 1, shape = 1, scale = 1) {
+
mediaGamma <- shape * scale
+
sigmaGamma <- sqrt(shape) * scale
+
a <- alfaFix + rnorm(n, sd = sigmaAlfa)
+
b <- betaFix + rnorm(n, sd = sigmaBeta)
+
a + x %o% b + matrix(((rgamma(n * lenth(x), shape = shape,
+
scale = scale) - mediaGamma)/sigmaGamma), ncol = n) *
+
sigmaRes
+ }
La proliferación de argumentos empeora si, por ejemplo, intervienen varias
gamma, como en una posible genMixNormGammaGamma.
Otras distribuciones necesitarı́an sus argumentos particulares... Y ası́ hasta
el infinito...
Una posibilidad es parametrizar mucho las funciones, es decir, encapsular
la generación de estos valores aleatorios en una función que admita como argumento alguna manera de codificar la distribución deseada:
4
>
+
+
+
+
+
+
+
+
+
>
+
+
+
+
+
+
+
genMix <- function(n = 1, x, alfaFix = 0, betaFix = 0, sigmaAlfa = 1,
sigmaBeta = 1, sigmaRes = 1, codiDistriAlfa = "norm", codiDistriBeta = "norm",
codiDistriRes = "norm", ...) {
a <- alfaFix + genera(n, sigma = sigmaAlfa, codiDistri = codiDistriAlfa,
...)
b <- betaFix + genera(n, sigma = sigmaBeta, codiDistri = codiDistriBeta,
...)
a + x %o% b + matrix(genera(n * length(x), sigma = sigmaRes,
codiDistri = codiDistriRes, ...), ncol = n)
}
genera <- function(n = 1, sigma, codiDistri = "norm", ...) {
if (codiDistri == "norm")
return(rnorm(n, sd = sigma))
else if (codiDistri == "cauchy")
return(rcauchy(n, sigma = sigma))
else if (codiDistri == "gamma")
return(escalaGamma(rgamma(n, ...), ...))
}
El código utiliza los parámetros “...’ y sigue con una interminable lista de
opciones if ... else .... Este enfoque es muy ineficiente tratándose de algo
que se va a ejecutar repetidamente. Tampoco es muy elegante la solución para
añadir una distribución no prevista previamente.
Además esta aproximación implica la proliferación de funciones auxiliares
para cada distribución concreta como por ejemplo la función escalaGamma asociada a los generadores de la distribución Gamma
> escalaGamma <- function(y, shape, scale) {
+
media <- shape * scale
+
desv <- sqrt(shape) * scale
+
return((y - media)/desv)
+ }
> escalaGamma(3, 2, 1.5)
[1] 0
> escalaGamma(3, 4, 1.5)
[1] -1
> genera(sigma = 3, codiDistri = "gamma", shape = 4, scale = 1.5)
[1] 1.927914
> genera(10, sigma = 3, codiDistri = "gamma", shape = 2, scale = 1.5)
[1] -0.22055242 -0.42705329 -1.09947202 0.67747385 -0.79676198
[7] -0.75023007 -1.03941319 -0.79414007 -0.81379519
5
0.05292618
> genera(10, sigma = 2.5)
[1] 0.1728964 1.9723468 0.5993931 -0.6624949
[7] -3.4581458 -2.5352884 -2.7833121 -2.1287941
0.4291814
3.4684306
> set.seed(127)
> genMix(nDatos, x, alfa0, beta0, sAlfa, sBeta, sRes)
[1,]
[2,]
[3,]
[4,]
[5,]
[6,]
[7,]
[8,]
[9,]
[10,]
[,1]
7.827101
11.143801
15.695551
22.412437
24.922213
28.517056
33.833978
41.124718
43.033940
45.787576
[,2]
5.503796
12.431905
15.903439
24.408809
29.557917
33.835794
33.612725
39.176276
43.400738
51.105699
[,3]
7.343257
13.508364
18.914433
22.468927
22.495091
24.086158
32.426765
37.865164
40.938131
44.820739
[,4]
7.982633
4.904760
7.058215
10.802503
15.548872
15.585602
16.861647
19.408568
22.363922
23.300257
> genMix(nDatos, x, alfa0, beta0, sAlfa, sBeta, sRes, codiDistriRes = "gamma",
+
shape = 2, scale = 1.5)
[1,]
[2,]
[3,]
[4,]
[5,]
[6,]
[7,]
[8,]
[9,]
[10,]
[,1]
7.732842
16.804009
25.846702
33.917161
37.037952
46.285301
56.524983
65.344616
68.832289
76.678248
[,2]
7.89982
11.87682
11.95843
17.45725
24.75232
29.75103
29.01189
34.09549
40.26083
46.40580
[,3]
[,4]
3.586113 14.08124
8.641406 24.29313
13.840130 29.11604
16.013519 41.22939
14.909064 52.56894
18.754935 63.03309
25.155685 66.96379
26.981861 81.77348
25.730369 90.66699
32.295908 101.31290
1.2
Una posible solución usando Environments
Puede encontrarse una alternativa a la sobreparametrización en funciones de
generación utilizando la capacidad de lexical scoping de R, asociada a su
concepto de “environment”. Nótese sin embargo que esta solución es aplicable a
R y no a otras implementaciones de S, como Splus.
> creaGenNormStandard <- function() {
+
return(function(n) {
+
rnorm(n, mean = 0, sd = 1)
+
})
+ }
6
>
+
+
+
+
>
+
+
+
+
+
+
creaGenCauchyStandard <- function() {
return(function(n) {
rcauchy(n, location = 0, scale = 1)
})
}
creaGenGammaStandard <- function(shape, scale) {
media <- shape * scale
desv <- sqrt(shape) * scale
return(function(n) {
(rgamma(n, shape = shape, scale = scale) - media)/desv
})
}
... etc
Serı́a necesario crear una función creaGen<Distribución>Standard por cada distribución que interesase. Estas funciones no tienen por que utilizar forzosamente las funciones incorporadas en R (rnorm, runif, etc), es decir que
podemos basarlas en nuestros propios algoritmos lo que resulta indispensable,
por ejemplo para distribuciones no previstas, como por ejemplo para generar la
distribución de Laplace.
De esta manera la función genera quedarı́a extremadamente corta y general,
mediante una llamada a una función “creadora de funciones que generan la distribución estandarizada adecuada”, bastarı́a crear una instancia de una función
adecuada para generar la distribución deseada, y pasarla como el argumento
stdGenerador:
> genera <- function(n = 1, sigma, stdGenerador) {
+
stdGenerador(n) * sigma
+ }
Ahora una función genMix adecuada y mucho más general serı́a: (por defecto
se generan factores aleatorios y residuos normales)
> genMix <- function(n = 1, x, alfaFix = 0, betaFix = 0, sigmaAlfa = 1,
+
sigmaBeta = 1, sigmaRes = 1, stdGenAlfa = creaGenNormStandard(),
+
stdGenBeta = creaGenNormStandard(), stdGenRes = creaGenNormStandard()) {
+
a <- alfaFix + genera(n, sigmaAlfa, stdGenAlfa)
+
b <- betaFix + genera(n, sigmaBeta, stdGenBeta)
+
a + x %o% b + matrix(genera(n * length(x), sigmaRes, stdGenRes),
+
ncol = n)
+ }
El código siguiente muestra como se utilizarı́an estas funciones:
> rn <- creaGenNormStandard()
> rn
7
function (n)
{
rnorm(n, mean = 0, sd = 1)
}
<environment: 0x01b2b218>
> rn(10)
[1] -0.45618218
[7] -0.84810970
0.35550018 0.50101130 -1.32365771 -1.08500359 -0.01433392
0.91355252 -1.34965843 0.89109798
> rg <- creaGenGammaStandard(shape = 4, scale = 1.5)
> rg
function (n)
{
(rgamma(n, shape = shape, scale = scale) - media)/desv
}
<environment: 0x01a43f8c>
> rg(10)
[1]
[7]
0.5508530 -1.0356482 -0.8366514 -0.6474291 -0.7156479 -0.6152633
0.1404982 0.4379852 -0.6353478 -0.8292362
> set.seed(237)
> rg(10)
[1] -0.02529546 -0.26041851
[7] 0.60631690 0.92880150
0.71239415 -1.42826501 -0.18677365 -0.93835653
0.47756935 0.52375029
> set.seed(237)
> (rgamma(10, shape = 4, scale = 1.5) - 6)/3
[1] -0.02529546 -0.26041851
[7] 0.60631690 0.92880150
0.71239415 -1.42826501 -0.18677365 -0.93835653
0.47756935 0.52375029
> set.seed(127)
> genMix(nDatos, x, alfa0, beta0, sAlfa, sBeta, sRes)
[1,]
[2,]
[3,]
[4,]
[5,]
[6,]
[7,]
[8,]
[9,]
[10,]
[,1]
7.827101
11.143801
15.695551
22.412437
24.922213
28.517056
33.833978
41.124718
43.033940
45.787576
[,2]
5.503796
12.431905
15.903439
24.408809
29.557917
33.835794
33.612725
39.176276
43.400738
51.105699
[,3]
7.343257
13.508364
18.914433
22.468927
22.495091
24.086158
32.426765
37.865164
40.938131
44.820739
8
[,4]
7.982633
4.904760
7.058215
10.802503
15.548872
15.585602
16.861647
19.408568
22.363922
23.300257
> rgRes <- creaGenGammaStandard(shape = 2, scale = 1.5)
> set.seed(127)
> genMix(nDatos, x, alfa0, beta0, sAlfa, sBeta, sRes, stdGenRes = rgRes)
[1,]
[2,]
[3,]
[4,]
[5,]
[6,]
[7,]
[8,]
[9,]
[10,]
[,1]
7.081417
10.407223
15.020807
24.369851
23.622664
28.139039
37.823764
37.823510
41.143164
47.833136
[,2]
13.18403
15.30297
15.28664
19.50172
26.36411
37.07451
37.63681
41.30462
54.37624
52.52497
[,3]
6.484199
8.277999
13.994917
18.934581
23.165119
27.352900
34.568167
40.241594
41.669352
44.071954
[,4]
2.366396
5.786712
7.079205
9.936757
14.069186
15.848008
16.310054
16.908162
23.333111
25.289833
> rgAlfa <- creaGenGammaStandard(shape = 1.5, scale = 2.5)
> genMix(nDatos, x, alfa0, beta0, sAlfa, sBeta, sRes, stdGenAlfa = rgAlfa,
+
stdGenRes = rgRes)
[1,]
[2,]
[3,]
[4,]
[5,]
[6,]
[7,]
[8,]
[9,]
[10,]
[,1]
3.993835
9.523836
7.049225
9.779640
11.828346
14.919247
13.213827
16.448773
16.713026
25.196619
[,2]
-3.843840
-6.028921
-9.491458
-11.824187
-10.732692
-23.736369
-25.956210
-30.783663
-36.060808
-38.436519
[,3]
9.248763
10.976857
13.661364
22.053730
25.858836
30.400311
32.190840
35.218472
40.815992
50.213557
[,4]
0.2703673
4.6747903
5.0825236
3.7583501
5.7498410
5.0696633
8.2649067
9.8778705
4.5316417
7.4920335
> rgBeta <- creaGenCauchyStandard()
> genMix(nDatos, x, alfa0, beta0, sAlfa, sBeta, sRes, stdGenAlfa = rgAlfa,
+
stdGenBeta = rgBeta, stdGenRes = rgRes)
[,1]
[,2]
[1,] 122.9116 4.493933
[2,] 241.0379 9.305928
[3,] 362.3312 9.962917
[4,] 478.9690 10.452612
[5,] 603.6862 15.918488
[6,] 721.2250 14.550723
[7,] 845.6951 19.276152
[8,] 961.3749 22.790262
[9,] 1082.3863 23.212232
[10,] 1200.2598 26.671966
[,3]
3.5066204
-0.2756740
1.9882551
-0.9024622
3.2437591
-2.1378910
-0.2557622
-4.7738150
-1.4563197
-1.7727417
9
[,4]
7.539529
9.921513
29.076111
24.711437
30.109254
33.740766
42.986800
44.164043
51.620688
54.678262
El principal inconveniente de esta solución, basada en el concepto R de environment, es su poca portabilidad a otras implementaciones de S. También
encontrarı́amos dificultades crecientes al querer dotarla de otras capacidades,
como la posibilidad de controlar de alguna manera las secuencias de números
aleatorios. Al fin y al cabo, esta solución se basa en el empleo de unos “objetos”
que se asocian a la definición de toda función en R y que “encapsulan” información referente al entorno en el que fue creada. Parece más razonable una
solución que nos permita un mayor control sobre dichos objetos (o similares), la
cual, como parece lógico, seguramente se halla en la “programación orientada a
objetos”, OOP.
2
2.1
Primera aproximación a la OOP
Clases y objetos
Retomando las ideas anteriores, nuestro objetivo es crear una función cuya estrucura ya está bastante clara:
> genMix <- function(n = 1, x, alfaFix = 0, betaFix = 0, sigmaAlfa = 1,
+
sigmaBeta = 1, sigmaRes = 1, distriAlfa, distriBeta, distriRes) {
+
a <- alfaFix + generaStd(distriAlfa, n) * sigmaAlfa
+
b <- betaFix + generaStd(distriBeta, n) * sigmaBeta
+
a + x %o% b + matrix(generaStd(distriRes, n * length(x)) *
+
sigmaRes, ncol = n)
+ }
Dentro de genMix, mediante la función generaStd, se darı́a la orden de
generar n valores estandarizados de acuerdo con la distribución deseada, para
posteriormente multiplicarlos por la escala adecuada. Esta distribución corresponderı́a al valor de los argumentos distriAlfa, distriBeta o distriRes de
genMix, “algo” (¡un objeto!) que tendrı́a que representar una distribución continua, con sus parámetros, con toda la información necesaria. Evidentemente,
quien mejor podrı́a conocer los detalles internos de cómo generar(se) serı́a este
“objeto” distribución, no la función generaStd.
Un primer paso es definir nuevas clases de datos, que representen distribuciones. De momento, parece razonable suponer que la información más significativa
asociada a una distribución son sus parámetros y los correspondientes valores
de dichos parámetros:
La creación de nuevas clases se realiza mediante la instrucción setClass.
Haciendo ? setClass se obtiene ayuda sobre su funcionamiento.
> setClass("Normal", representation(media = "numeric", sigma = "numeric"),
+
prototype(media = 0, sigma = 1))
[1] "Normal"
10
> setClass("Cauchy", representation(mediana = "numeric", escala = "numeric"),
+
prototype(mediana = 0, escala = 1))
[1] "Cauchy"
> setClass("Gamma", representation(forma = "numeric", escala = "numeric",
+
media = "numeric", sigma = "numeric"), prototype(forma = 1,
+
escala = 1))
[1] "Gamma"
La función setClass admite numerosos argumentos. De momento solamente
nos centraremos en los tres primeros. El primero, Class, es indispensable. Especifica el nombre de la clase que se está creando. El segundo, representation
especifica la representación, la estructura en el sentido de los “slots” o campos
de datos, que van a tener los “objetos” o “instancias” concretas de la clase. En
una primera aproximación, estos “slots” se pueden asimilar a los campos de una
lista de un objeto de la clase predefinida list. El tercer argumento, prototype, especifica los valores iniciales de los campos en cualquier instancia u objeto
recién creado (a no ser que, de las posibles maneras que luego discutiremos,
explı́citamente indiquemos que van a tener otro valor distinto).
El valor asignado a los argumentos representation y prototype es en realidad una lista o algo convertible en una lista. De todas maneras lo habitual
es emplear las funciones representation y prototype (que tienen el mismo
nombre que los correspondientes argumentos de setClass) y que generan representaciones y prototipos de una forma ordenada, con la sintaxis indicada en
los ejemplos, es decir, indicando la clase de cada slot (predefinida, como numeric en los ejemplos, o una nueva clase definida en otra llamada a setClass) o
el valor inicial de cada slot, respectivamente. Es decir, una llamada a setClass
como:
> setClass(Class = "Normal", representation = representation(media = "numeric",
+
sigma = "numeric"), prototype = prototype(media = 0, sigma = 1))
[1] "Normal"
serı́a equivalente, pero más prolija, a la del ejemplo inicial.
Los argumentos representation y prototype son opcionales. Si se prescinde de prototype (y de otras maneras de especificar valores iniciales que veremos
más adelante) o determinados slots no se incluyen en prototype, dichos slots
tienen el valor “nulo” propio de su clase, a no confundir con el valor “cero”.
Por ejemplo, si en la definición de la clase Normal se hubiese prescindido de
prototype, el valor del campo media serı́a numeric(0), un vector numérico de
longitud 0, que evidentemente no es lo mismo que media = 0.
Aunque pueda parecer de momento extraño, una clase puede carecer también
de representación.
Para crear una variable que contenga datos de aquella clase, es decir, una
variable cuyo nombre represente a una “instancia” u “objeto” emplearemos la
función new. Ası́, para crear una instancia de la clase Normal indicaremos:
11
> normal <- new("Normal")
> normal
An object of class "Normal"
Slot "media":
[1] 0
Slot "sigma":
[1] 1
Otra instancia de la clase “Normal”, ahora de parámetros -1 y 3 se crearı́a ası́:
> normal2 <- new("Normal", media = -1, sigma = 3)
> normal2
An object of class "Normal"
Slot "media":
[1] -1
Slot "sigma":
[1] 3
etc.
Para acceder a los campos de un objeto de cierta clase, emplearemos el
operador @, de forma similar a como emplearı́amos el operador $ para listas.
(Cuidado: AMBOS OPERADORES NO SON INTERCAMBIABLES. @ sirve
para acceder a los campos de un objeto de una clase, $ se utiliza en listas.)
> normal@sigma
[1] 1
> normal2@sigma
[1] 3
> normal2@media
[1] -1
Hay una importante diferencia entre @ y $, y entre el concepto de lista y de
clase y sus instancias: en una lista, la definición de los “slots” o “campos” está
asociada a cada variable concreta (varias listas que tuviesen exactamente los
mismos campos tendrı́an cada una de ellas una copia de las definiciones de estos
campos), en cambio, las instancias de una clase definida mediante setClass no
contienen la definición de estos campos, esta definición está asociada a la clase.
Obsérvese que si los slots que definen una clase y sus instancias son de
una clase ya definida por R (numeric, list, etc), si al crear un objeto no se
les ha asignado un valor inicial mediante prototype, o mediante argumentos
12
a new o mediante un procedimiento más elaborado que veremos más adelante
(sobreescribir el método initialize), dichos slots tienen valor inicial “nulo” (lo
que signifique dicho valor para cada clase): fijémonos en el valor de media y
sigma de los objetos de clase Gamma:
> cauchy <- new("Cauchy")
> cauchy
An object of class "Cauchy"
Slot "mediana":
[1] 0
Slot "escala":
[1] 1
> gamm <- new("Gamma")
> gamm
An object of class "Gamma"
Slot "forma":
[1] 1
Slot "escala":
[1] 1
Slot "media":
numeric(0)
Slot "sigma":
numeric(0)
> gamm2 <- new("Gamma", forma = 2, escala = 3)
> gamm2
An object of class "Gamma"
Slot "forma":
[1] 2
Slot "escala":
[1] 3
Slot "media":
numeric(0)
Slot "sigma":
numeric(0)
Es evidente que todavı́a queda trabajo por hacer para que la clase Gamma
tenga un comportamiento adecuado.
13
2.2
Los métodos describen el comportamiento de las clases
De momento las clases y los objetos anteriores sirven de bien poco, solamente
para almacenar los valores de parámetros, y para eso ya tenemos las listas. Hay
que asociarlas a comportamientos. Para ello, en primer lugar se define una
“función genérica” generaStd:
> setGeneric("generaStd", function(distri, ...) standardGeneric("generaStd"))
[1] "generaStd"
setGeneric es una función que crea otras funciones, similar a ejemplos que
hemos visto anteriormente. La llamada es un poco rara pero es la manera más
cómoda y segura de crear funciones “genéricas”.
La llamada anterior ha creado una función genérica de nombre generaStd.
Esta función en realidad solamente es una especie de interfaz abstracta. Espera
recibir un argumento, distri, y posibles argumentos adicionales, de momento
sin concretar (...).
Según cual sea la clase real de distri, se ejecutará una función especı́fica,
un “método” que implementará esta función genérica para cada distribución
concreta.
El código siguiente muestra cómo se utiliza la función setMethod para crear
el método generaStd asociado a distri de clase Normal:
> setMethod("generaStd", signature("Normal"), function(distri,
+
n = 1) {
+
rnorm(n)
+ })
[1] "generaStd"
De manera análoga, para las otras clases:
> setMethod("generaStd", signature("Cauchy"), function(distri,
+
n = 1) {
+
rcauchy(n)
+ })
[1] "generaStd"
> setMethod("generaStd", signature("Gamma"), function(distri, n = 1) {
+
(rgamma(n, shape = distri@forma, scale = distri@escala) +
distri@media)/distri@sigma
+ })
[1] "generaStd"
14
Los tres primeros argumentos de setMethod, los más importantes, corresponden al nombre del método, a su “signatura” y a la definición de la función
que realmente va a implementar el método. La signatura establece a que clase (o
a que clases, ya que el modelo de objetos S4 implementa “despachado múltiple”,
una caracterı́stica que no trataremos de momento) se asocia el método.
getMethods("generaStd") lista los métodos asociados esta función genérica.
Finalmente veamos cómo se utiliza la función genérica generaStd con objetos de clases ’Normal’ y ’Cauchy’:
> generaStd(normal, 25)
[1] 0.20759794 0.35422699 -0.04298372 -0.52484457 0.41128229 -0.56953489
[7] -0.94738810 -0.40783714 0.01902878 0.50074461 0.82340699 0.72844425
[13] -1.65343978 -2.17757446 0.02775334 -0.49487201 -0.83308335 2.43341694
[19] 0.03756152 0.91065071 -0.46258200 1.81222765 -0.19598847 -0.72454848
[25] 0.83344619
> generaStd(normal2)
[1] 0.4505594
> generaStd(cauchy, 10)
[1] -0.479049541 6.472337442
[6] 1.353520511 -0.006085034
1.856003970 -4.831934684 0.924744519
0.859242677 3.074901767 -3.251580586
Si se emplea del depurador de R o simplemente se intercala una instrucción print con un mensaje adecuado en la definición de los métodos definidos
mediante SetMethod, se puede comprobar que en realidad generaStd(normal,
25) se ha traducido en la ejecución del método generaStd asociado a la clase
Normal mientras que generaStd(cauchy, 10) ha ejecutado el correspondiente
método de la clase Cauchy.
De momento lo anterior no funcionarı́a con la clase Gamma:
> generaStd(gamm)
numeric(0)
> generaStd(gamm2, 5)
numeric(0)
El problema está en que los slots media y sigma no tienen un valor adecuado:
> gamm@media
numeric(0)
15
> gamm2@sigma
numeric(0)
Para solucionarlo, debemos saber algo más de la inicialización de instancias
de una clase.
Resumiendo los conceptos anteriores, el código básico necesario para crear
una clase y sus métodos consiste en:
ˆ
Cada funcionalidad que se desea asociar con una o más clases debe corresponder a una función genérica. Cada una de ellas se define mediante una
llamada a setGeneric. La funcionalidad definida en abstracto mediante
la función genérica será posteriormente implementada de forma explı́cita
en cada clase, al definir sus métodos asociados.
ˆ
Mediante una llamada a la función setClass que define cada clase y su
estructura.
ˆ
Mediante setMethod se definen los métodos especı́ficos que implementan
las funcionalidades definidas genéricamente en setGeneric.
El código siguiente ilustra estos pasos para la clase Normal:
> setGeneric("generaStd", function(distri, ...) standardGeneric("generaStd"))
[1] "generaStd"
> setClass("Normal", representation(media = "numeric", sigma = "numeric"),
+
prototype(media = 0, sigma = 1))
[1] "Normal"
> setMethod("generaStd", signature("Normal"), function(distri,
+
n = 1) {
+
rnorm(n)
+ })
[1] "generaStd"
Si la única funcionalidad deseada es generaStd, al crear nuevas clases sólo
nos hará falta definirlas mediante setClass y definir sus métodos especı́ficos
para generaStd mediante setMethod.
> setClass("Cauchy", representation(mediana = "numeric", escala = "numeric"),
+
prototype(mediana = 0, escala = 1))
[1] "Cauchy"
> setMethod("generaStd", signature("Cauchy"), function(distri,
+
n = 1) {
+
rcauchy(n)
+ })
16
[1] "generaStd"
> setClass("Gamma", representation(forma = "numeric", escala = "numeric",
+
media = "numeric", sigma = "numeric"), prototype(forma = 1,
+
escala = 1))
[1] "Gamma"
> setMethod("generaStd", signature("Gamma"), function(distri, n = 1) {
+
(rgamma(n, shape = distri@forma, scale = distri@escala) +
distri@media)/distri@sigma
+ })
[1] "generaStd"
2.3
Métodos de inicialización
Como ya se ha indicado, los problemas detectados con la clase Gamma residen
principalmente en que los slots media y sigma no tienen un valor inicial adecuado. De nada sirve darles valor inicial fijo mediante prototype, ya que estos
slots tienen un valor que debe calcularse a partir de forma y escala (en realidad
solamente enmascaramos un error; ahora funcionará, pero mal):
> setClass("Gamma", representation(forma = "numeric", escala = "numeric",
+
media = "numeric", sigma = "numeric"), prototype(forma = 1,
+
escala = 1, media = 1, sigma = 1))
[1] "Gamma"
> gamm2 <- new("Gamma", forma = 2, escala = 3)
> gamm2
An object of class "Gamma"
Slot "forma":
[1] 2
Slot "escala":
[1] 3
Slot "media":
[1] 1
Slot "sigma":
[1] 1
> generaStd(gamm2, 5)
[1] 1.3746805 8.3677438 0.9432062 4.4109584 8.1955630
17
Por desgracia, no son admisibles expresiones como la siguiente, en prototype:
> setClass("Gamma",
+
representation(
+
forma = "numeric",
+
media = "numeric",
+
),
+
prototype(forma = 1,
+ )
Error in .prototype(...)
>
escala = "numeric",
sigma = "numeric"
escala = 1, media = forma*escala, sigma = sqrt(forma)*escala)
: object "forma" not found
Una solución obvia serı́a inicializar “a mano” media y sigma al crear el objeto:
> gamm2 <- new("Gamma", forma = 2, escala = 3, media = 2 * 3, sigma = sqrt(2) *
+
3)
> gamm2
An object of class "Gamma"
Slot "forma":
[1] 2
Slot "escala":
[1] 3
Slot "media":
[1] 6
Slot "sigma":
[1] 4.242641
> generaStd(gamm2, 5)
[1] -0.30678167
3.10735412
0.07901555 -0.52918493
0.66619903
Aunque es una solución muy poco adecuada: obliga al usuario de la clase y
de la función genérica a acordarse de (y a aplicar) las expresiones de la media
y de la desviación tı́pica de una gamma. Demasiado complicado y propenso a
errores
Otra solución obvia estarı́a en calcular media y sigma dentro del método
generaStd:
> setMethod("generaStd", signature("Gamma"), function(distri, n = 1) {
+
media <- distri@forma * distri@escala
+
sigma <- sqrt(distri@forma) * distri@escala
+
(rgamma(n, shape = distri@forma, scale = distri@escala) +
media)/sigma
+ })
18
[1] "generaStd"
> gamm <- new("Gamma")
> gamm
An object of class "Gamma"
Slot "forma":
[1] 1
Slot "escala":
[1] 1
Slot "media":
[1] 1
Slot "sigma":
[1] 1
> gamm2 <- new("Gamma", forma = 2, escala = 3)
> gamm2
An object of class "Gamma"
Slot "forma":
[1] 2
Slot "escala":
[1] 3
Slot "media":
[1] 1
Slot "sigma":
[1] 1
> generaStd(gamm)
[1] -0.931548
> generaStd(gamm2, 5)
[1]
0.3203107
1.0549065
0.5712505
0.6911388 -0.2689783
La generación funciona bien a pesar de los valores absurdos de los slots media
y sigma. En realidad no se utilizan, ahora media y sigma son variables locales
del método; bajo este enfoque podrı́amos prescindir completamente de los slots
correspondientes.
Lo anterior funcionarı́a pero no es lo más eficiente, es mejor disponer de
slots media y sigma ya previamente calculados (de una vez por todas, al crear
el objeto), no perder tiempo recalculándolos cada vez que se activa el método.
19
Una solución mucho mejor es crear un método initialize. Dicho método
forzosamente debe tener este nombre, initialize, y forzosamente debe tener un
primer argumento de nombre .Object seguido de los argumentos que necesitarı́a
para una adecuada inicialización -los mismos que pondrı́amos en new.
> setMethod("initialize", signature("Gamma"), function(.Object,
+
forma = 1, escala = 1) {
+
.Object@forma <- forma
+
.Object@escala <- escala
+
.Object@media <- forma * escala
+
.Object@sigma <- sqrt(forma) * escala
+
.Object
+ })
[1] "initialize"
El método initialize NO se suele llamar directamente a través de su función genérica (es decir, no se escribe initialize(distri, etc) si no que se
llama automáticamente al hacer new, con los argumentos que se indicaron a
new después del nombre de la clase. En realidad lo que hemos hecho ha sido
substituir un método initialize, que R crearı́a por defecto, por el nuestro
propio. Al crear un objeto mediante new, en primer lugar se ejecuta lo indicado
en prototype (si existe) y a continuación se ejecuta initialize. Ambas formas de inicialización pueden coexistir, pero a menudo un método initialize
adecuado hace innecesario prototype. Ciertamente en la definición de la clase
Gamma podemos suprimir prototype:
> setClass("Gamma", representation(forma = "numeric", escala = "numeric",
+
media = "numeric", sigma = "numeric"))
[1] "Gamma"
... y devolver el método generaStd a su forma original:
> setMethod("generaStd", signature("Gamma"), function(distri, n = 1) {
+
(rgamma(n, shape = distri@forma, scale = distri@escala) +
distri@media)/distri@sigma
+ })
[1] "generaStd"
> gamm <- new("Gamma")
> gamm
An object of class "Gamma"
Slot "forma":
[1] 1
20
Slot "escala":
[1] 1
Slot "media":
[1] 1
Slot "sigma":
[1] 1
> gamm2 <- new("Gamma", forma = 2, escala = 3)
> gamm2
An object of class "Gamma"
Slot "forma":
[1] 2
Slot "escala":
[1] 3
Slot "media":
[1] 6
Slot "sigma":
[1] 4.242641
> generaStd(gamm)
[1] 0.3192866
> generaStd(gamm2, 5)
[1]
1.44488888 -0.04631383
0.18740485 -0.28510912 -1.15902442
initialize tal como está definido anteriormente nos protege de errores de
definición de slots:
> gamm2 <- new("Gamma", forma = 2, escala = 3, media = 1, sigma = 1)
Error in .local(.Object, ...) : unused argument(s) (media = 1, sigma = 1)
>
Ahora no es posible (erróneamente) modificar slots no previstos como argumentos de initialize.
2.4
Validez de las instancias de una clase
La mejor forma de controlar que los objetos creados mediante new sean instancias
válidas de su clase es la creación de un método de validación. Se puede asociar
un método de validación a una clase en el momento de crearla, mediante el
argumento validity de setClass, o con posterioridad a la creación de la clase.
En el momento de definir la clase serı́a:
21
> setClass("Gamma", representation(forma = "numeric", escala = "numeric",
+
media = "numeric", sigma = "numeric"), validity = function(object) {
+
if ((object@forma <= 0) || (object@escala <= 0))
+
return("Los parámetros de una Gamma deben ser positivos")
+
else return(TRUE)
+ })
[1] "Gamma"
> gamm <- new("Gamma", escala = -1)
> validObject(gamm)
Error in validObject(gamm) :
invalid class "Gamma" object: Los parámetros de una Gamma deben ser positivos
>
Con posterioridad a la definición de la clase serı́a:
> setClass("Gamma", representation(forma = "numeric", escala = "numeric",
+
media = "numeric", sigma = "numeric"))
[1] "Gamma"
> setValidity("Gamma", function(object) {
+
if ((object@forma <= 0) || (object@escala <= 0))
+
return("Los parámetros de una Gamma deben ser positivos")
+
else return(TRUE)
+ })
Slots:
Name:
forma escala
media
sigma
Class: numeric numeric numeric numeric
> gamm <- new("Gamma", escala = -1)
> validObject(gamm)
Error in validObject(gamm) :
invalid class "Gamma" object: Los parámetros de una Gamma deben ser positivos
>
Nótese que un método de validación debe devolver un valor lógico TRUE
si todo ha ido bien, o una o más cadenas de caracteres si ha habido algún
problema. Nótese también que new no llama automáticamente al método de
validación, hay que hacerlo explı́citamente mediante la función validObject.
El método initialize creado por R automáticamente a partir de la cláusula prototype de setClass llama al método de validación automáticamente,
en cambio nuestra initialize tiene que llamar explı́citamente dicho método,
mediante validObject:
22
> setMethod("initialize", signature("Gamma"), function(.Object,
+
forma = 1, escala = 1) {
+
.Object@forma <- forma
+
.Object@escala <- escala
+
.Object@media <- forma * escala
+
.Object@sigma <- sqrt(forma) * escala
+
validObject(.Object)
+
.Object
+ })
[1] "initialize"
> setValidity("Normal", function(object) {
+
if (object@sigma < 0)
+
return("La desviación estándar no puede ser negativa")
+
else return(TRUE)
+ })
Slots:
Name:
media
sigma
Class: numeric numeric
> setValidity("Cauchy", function(object) {
+
if (object@escala < 0)
+
return("El parámetro de escala no puede ser negativo")
+
else return(TRUE)
+ })
Slots:
Name: mediana escala
Class: numeric numeric
> normal <- new("Normal", sigma = -2)
Error in validObject(.Object) :
invalid class "Normal" object: La desviación estándar no puede ser negativa
>
Nótese que para las clases Normal y Cauchy, para las cuales no se ha definido
explı́citamente initialize, dentro de new se activa automáticamente el método
de validación, cosa que no ocurre con Gamma. En esta clase necesitamos llamar
explı́citamente validObject.
2.5
Recapitulación del código anterior
Mezclar definiciones de clases y métodos con código que ejecuta instrucciones
con estas clases y métodos (como estamos haciendo aquı́) es algo muy confuso
23
y propenso a errores. Aunque R (o S) no nos impone ninguna restricción en
este sentido, es mejor procurar organizar las definiciones en ficheros aparte, de
forma similar a lo propuesto en el fichero classesDistr1.r.
Para probarlo serı́a recomendable dejar el área de trabajo en blanco:
> rm(list = ls())
y a continuación procesar dicho fichero:
> source("classesDistr1.r")
> ls()
[1] "generaStd"
Finalmente podemos comprobar que el código anterior funciona razonablemente bien:
> genMix <- function(n = 1, x, alfaFix = 0, betaFix = 0, sigmaAlfa = 1,
+
sigmaBeta = 1, sigmaRes = 1, distriAlfa = new("Normal"),
+
distriBeta = new("Normal"), distriRes = new("Normal")) {
+
a <- alfaFix + generaStd(distriAlfa, n) * sigmaAlfa
+
b <- betaFix + generaStd(distriBeta, n) * sigmaBeta
+
a + x %o% b + matrix(generaStd(distriRes, n * length(x)) *
+
sigmaRes, ncol = n)
+ }
> genMix(5, x = 1:10)
[1,]
[2,]
[3,]
[4,]
[5,]
[6,]
[7,]
[8,]
[9,]
[10,]
>
>
>
>
>
>
>
>
[,1]
2.0697840
-0.7607013
1.4491276
-1.1827059
-0.1283908
0.9900569
1.4123577
1.3320416
-1.2460791
-1.1112227
[,2]
[,3]
[,4]
2.277017
0.2158243 2.0184514
4.043109 -0.4983071 -2.5836715
7.838791 -0.2435911 3.1205457
5.734360 -5.9404333 -0.3997516
8.582717 -6.9567295 -1.7199717
14.630149 -3.0219838 2.4031796
13.814263 -7.6887180 2.3002664
14.564912 -7.4611774 2.4285752
15.234465 -11.9306318 2.4311031
17.600898 -11.8033975 -0.0928880
[,5]
1.497798
2.213557
4.306410
2.832477
1.949985
6.520411
6.596844
9.678610
6.067136
8.018764
alfa0 <- 3
beta0 <- 2.5
sAlfa <- 1.5
sBeta <- 2.5
sRes <- 2
nDatos <- 5
x <- 1:10
genMix(nDatos, x, alfa0, beta0, sAlfa, sBeta, sRes)
24
[1,]
[2,]
[3,]
[4,]
[5,]
[6,]
[7,]
[8,]
[9,]
[10,]
[,1]
2.455668
7.954966
5.381788
15.944346
18.490598
15.096101
22.641620
27.662357
30.937799
29.076595
[,2]
2.124608
7.835064
10.810310
12.712536
12.225715
11.060056
15.168481
21.374174
22.309157
21.728526
[,3]
3.592765
4.102866
7.785552
6.734521
6.529494
11.441761
13.337335
11.023567
10.200572
16.334061
[,4]
6.532528
9.092708
14.344240
14.491853
21.779590
22.985215
28.580660
36.863379
34.328924
37.173899
[,5]
0.1596465
2.1254504
-4.5172319
-3.3507111
-3.9570104
-3.6273872
-3.0148443
-7.2811764
-2.4567795
-8.7206779
> genMix(nDatos, x, alfa0, beta0, sAlfa, sBeta, sRes, distriRes = new("Gamma",
+
forma = 4, escala = 2))
[,1]
[,2]
[,3]
[,4]
[,5]
[1,] 5.828626 4.085989 8.81076441 8.641802 7.128532
[2,] 4.596794 14.146652 3.72308561 20.660674 11.869066
[3,] 4.493374 8.581463 2.59470386 24.234933 15.709161
[4,] 7.521117 13.191167 1.01267708 31.475014 15.653900
[5,] 7.578287 16.236669 1.18592002 39.819608 19.739553
[6,] 6.802906 20.311844 2.06658199 48.358449 25.939607
[7,] 8.748973 24.225701 4.30246307 53.151488 31.828219
[8,] 7.316266 24.686034 -3.40939134 60.232537 34.361657
[9,] 12.809737 26.882822 1.64069669 65.170838 31.782248
[10,] 10.915724 30.924208 -0.06626341 73.497244 36.444531
> alfas <- new("Cauchy")
> resids <- new("Gamma", forma = 4, escala = 2)
> genMix(nDatos, x, alfa0, beta0, sAlfa, sBeta, sRes, distriAlfa = alfas,
+
distriRes = resids)
[1,]
[2,]
[3,]
[4,]
[5,]
[6,]
[7,]
[8,]
[9,]
[10,]
[,1]
16.290104
7.421282
11.322315
14.231306
23.223106
33.860229
23.974477
29.022385
29.394093
40.065105
[,2]
13.881975
4.268405
10.529196
12.121528
17.185228
29.922209
19.402730
23.379986
25.723306
36.658547
[,3]
14.641736
2.539107
1.684035
4.477441
10.968053
19.883294
13.648584
7.717834
10.234341
15.072369
[,4]
19.32294
16.26572
22.41910
35.28531
39.67759
57.82842
52.58550
61.86270
69.14995
82.18378
[,5]
20.05648
12.90337
18.33077
26.96214
34.18940
49.22408
44.56638
50.89345
57.58529
68.78922
>badGamma <- new("Gamma", forma = -1, escala = 3)
Error in validObject(.Object) : invalid class "Gamma" object:
Los parámetros de una Gamma deben ser positivos
25
In addition: Warning message:
NaNs produced in: sqrt(forma)
2.6
Unas clases distribución algo más completas
Para finalizar esta parte, vamos a crear unas clases que implementarán distribuciones y que tendrán unas funcionalidades algo más ricas. Si nos fijamos en las
funciones que R proporciona para manejar distribuciones, parece claro que se
considera que las funcionalidades básicas asociadas a este concepto son el cálculo
de la función de densidad, el cálculo de la función de distribución, el cálculo de
cuantiles y la generación de valores aleatorios. Vamos a reproducir este esquema
en las clases a definir. En realidad, “generar un valor estandarizado” puede ser
interesante pero no parece tanto una competencia básica de estas clases, aunque
la mantendremos. Similarmente podrı́amos pensar en multitud de competencias
(en el fondo, funciones genéricas y sus correspondientes métodos) interesantes:
cálculo de la esperanza, cálculo de la varianza, función de verosimilitud y un
largo etc. En general un buen diseño implica, entre otras cosas, una cierta
contención en las competencias asociadas a cada clase. Posiblemente las clases
distribución se podrı́an limitar a las cuatro competencias listadas antes, otros
cálculos, tales como generar valores estandarizados y operadores diversos sobre
distribuciones, podrı́an ser competencia de otras clases que interaccionasen con
las clases “distribución”. Como nuestro propósito es introducirnos en la orientación a objetos (OOP), y no impartir un curso de análisis y diseño OOP, no
consideraremos estas posibilidades.
Fijémonos en que casi todo consistirá en crear nuevas funciones genéricas e
implementarlas en cada clase concreta mediante los métodos correspondientes.
> setGeneric("dens", function(distri, ...) standardGeneric("dens"))
[1] "dens"
> setGeneric("distri", function(distri, ...) standardGeneric("distri"))
[1] "distri"
> setGeneric("cuantil", function(distri, ...) standardGeneric("cuantil"))
[1] "cuantil"
> setGeneric("genera", function(distri, ...) standardGeneric("genera"))
[1] "genera"
> setGeneric("generaStd", function(distri, ...) standardGeneric("generaStd"))
[1] "generaStd"
A continuación es necesario definir las clases y los métodos asociados a ellas,
que implementan las funciones genéricas:
26
> setClass("Normal", representation(media = "numeric", sigma = "numeric"),
+
prototype(media = 0, sigma = 1))
[1] "Normal"
> setValidity("Normal", function(object) {
+
if (object@sigma < 0)
+
return("La desviación estándar no puede ser negativa")
+
else return(TRUE)
+ })
Slots:
Name:
media
sigma
Class: numeric numeric
> setMethod("dens", signature("Normal"), function(distri, x) {
+
dnorm(x, mean = distri@media, sd = distri@sigma)
+ })
[1] "dens"
> setMethod("distri", signature("Normal"), function(distri, x) {
+
pnorm(x, mean = distri@media, sd = distri@sigma)
+ })
[1] "distri"
> setMethod("cuantil", signature("Normal"), function(distri, p) {
+
qnorm(p, mean = distri@media, sd = distri@sigma)
+ })
[1] "cuantil"
> setMethod("genera", signature("Normal"), function(distri, n = 1) {
+
rnorm(n, mean = distri@media, sd = distri@sigma)
+ })
[1] "genera"
> setMethod("generaStd", signature("Normal"), function(distri,
+
n = 1) {
+
rnorm(n)
+ })
[1] "generaStd"
> setClass("Cauchy", representation(mediana = "numeric", escala = "numeric"),
+
prototype(mediana = 0, escala = 1))
27
[1] "Cauchy"
> setValidity("Cauchy", function(object) {
+
if (object@escala < 0)
+
return("El parámetro de escala no puede ser negativo")
+
else return(TRUE)
+ })
Slots:
Name: mediana escala
Class: numeric numeric
> setMethod("dens", signature("Cauchy"), function(distri, x) {
+
dcauchy(x, location = distri@mediana, scale = distri@escala)
+ })
[1] "dens"
> setMethod("distri", signature("Cauchy"), function(distri, x) {
+
pcauchy(x, location = distri@mediana, scale = distri@escala)
+ })
[1] "distri"
> setMethod("cuantil", signature("Cauchy"), function(distri, p) {
+
qcauchy(p, location = distri@mediana, scale = distri@escala)
+ })
[1] "cuantil"
> setMethod("genera", signature("Cauchy"), function(distri, n = 1) {
+
rcauchy(n, location = distri@mediana, scale = distri@escala)
+ })
[1] "genera"
> setMethod("generaStd", signature("Cauchy"), function(distri,
+
n = 1) {
+
rcauchy(n)
+ })
[1] "generaStd"
> setClass("Gamma", representation(forma = "numeric", escala = "numeric",
+
media = "numeric", sigma = "numeric"))
[1] "Gamma"
28
> setValidity("Gamma", function(object) {
+
if ((object@forma <= 0) || (object@escala <= 0))
+
return("Los parámetros de una Gamma deben ser positivos")
+
else return(TRUE)
+ })
Slots:
Name:
forma escala
media
sigma
Class: numeric numeric numeric numeric
> setMethod("initialize", signature("Gamma"), function(.Object,
+
forma = 1, escala = 1) {
+
.Object@forma <- forma
+
.Object@escala <- escala
+
.Object@media <- forma * escala
+
.Object@sigma <- sqrt(forma) * escala
+
validObject(.Object)
+
.Object
+ })
[1] "initialize"
> setMethod("dens", signature("Gamma"), function(distri, x) {
+
dgamma(x, shape = distri@forma, scale = distri@escala)
+ })
[1] "dens"
> setMethod("distri", signature("Gamma"), function(distri, x) {
+
pgamma(x, shape = distri@forma, scale = distri@escala)
+ })
[1] "distri"
> setMethod("cuantil", signature("Gamma"), function(distri, p) {
+
qgamma(p, shape = distri@forma, scale = distri@escala)
+ })
[1] "cuantil"
> setMethod("genera", signature("Gamma"), function(distri, n = 1) {
+
rgamma(n, shape = distri@forma, scale = distri@escala)
+ })
[1] "genera"
> setMethod("generaStd", signature("Gamma"), function(distri, n = 1) {
+
(rgamma(n, shape = distri@forma, scale = distri@escala) +
distri@media)/distri@sigma
+ })
29
[1] "generaStd"
El fichero classesDistr1Complet.r contiene todas las definiciones anteriores. El siguiente código realiza distintas pruebas sobre este código:
>
>
>
>
>
>
normal <- new("Normal")
normal2 <- new("Normal", media = -1, sigma = 3)
cauchy <- new("Cauchy")
gamm <- new("Gamma")
gamm2 <- new("Gamma", forma = 2, escala = 3)
dens(normal, 0)
[1] 0.3989423
> distri(normal, 0)
[1] 0.5
> cuantil(normal, 0.5)
[1] 0
> genera(normal, 10)
[1] -0.37374819
[7] 1.56906982
0.64942865 -0.29007155 0.53817866 -0.70395722
0.06967089 0.71524575 -1.56264541
2.59823846
> dens(normal2, -1)
[1] 0.1329808
> distri(normal2, -1)
[1] 0.5
> cuantil(normal2, 0.5)
[1] -1
> genera(normal2, 10)
[1]
[6]
-3.17392355
2.38978188
-0.04667653 -1.39782558
-2.86283696 -10.08171511
> dens(cauchy, 0)
[1] 0.3183099
> distri(cauchy, 0)
[1] 0.5
30
2.20209959
-2.50947288
-2.63208891
-2.12430239
> cuantil(cauchy, 0.5)
[1] -6.123032e-17
> genera(cauchy, 10)
[1]
[7]
2.5553133 -1.8795099
0.5129506 -0.5512028
0.9686760 -0.1901093 -0.4878202
0.4135581 0.3192422
2.5662124
> dens(gamm, 1)
[1] 0.3678794
> distri(gamm, 1)
[1] 0.6321206
> cuantil(gamm, 0.5)
[1] 0.6931472
> genera(gamm, 10)
[1] 0.94230581 1.46280818 1.28735629 0.41175790 0.07865940 3.63623306
[7] 0.04200269 1.11798145 0.26932689 1.51616092
> dens(gamm2, 1)
[1] 0.07961459
> distri(gamm2, 1)
[1] 0.04462492
> cuantil(gamm2, 0.5)
[1] 5.035041
> genera(gamm2, 10)
[1] 3.830175 8.138981 1.469722
[8] 18.901635 13.584714 10.818630
3
5.574794
4.531878
3.465197
Si es OOP tiene herencia
En los ejemplos anteriores no hemos utilizado una de las caracterı́sticas definitorias y más útiles de la OOP (y también la más sobreutilizada). Nos estamos
refiriendo a que entre clases se puede establecer una relación que a veces se la
denomina “herencia”, otras veces “especialización” y curiosamente otras veces
“generalización” –de hecho no es ninguna contradicción, depende de en qué sentido se considere dicha relación. También se habla a veces de “extensión”, en el
sentido que una clase que especializa a otra lo hace, normalmente, añadiendo
slots y métodos. Otro término frecuente es el inglés “subclassing”, de difı́cil
traducción, entre otras maneras de denominar este concepto.
31
8.659923
3.1
Una normal es una absolutamente continua
Tras un curso de estadı́stica general bien aprovechado, en general queda claro
que la Gamma es una distribución absolutamente continua, de la misma manera que lo son la Normal y la distribución de Cauchy. En otros términos, las
tres distribuciones anteriores son especializaciones (o derivan) del concepto de
distribución absolutamente continua (o a la inversa, dicho concepto generaliza
los de Gamma, Cauchy y Normal). También es razonable decir que estas tres
distribuciones “heredan” las caracterı́sticas de “absolutamente continua”, en el
sentido de que todo acción (¡función genérica y/o método!) que tenga sentido para una absolutamente continua, deberá continuar teniendo sentido para
cualquiera de ellas. Y no solamente para las acciones, todo slot o campo con
información relevante en una hipotética clase “absolutamente continua” tendrı́a
que tener sentido en los objetos de sus clases derivadas. Es por lo tanto razonable que una clase como Gamma herede todos los slots y métodos que se hayan
definido en la clase más general, es decir, que sus instancias posean dichas caracterı́sticas. En general esto es ası́ y funciona bien. En ocasiones se tienen
algunas situaciones paradójicas que casi siempre se pueden asociar a un mal uso
de la relación de herencia.
El fichero classesDistr2.r ilustra los conceptos anteriores en R. En este
ejemplo se introduce la relación de herencia en la cláusula representation de
las clases derivadas. Fijémonos en el siguiente fragmento de código:
> setClass("DistribucionAbsContinua")
[1] "DistribucionAbsContinua"
> setClass("Normal", representation("DistribucionAbsContinua",
+
media = "numeric", sigma = "numeric"), prototype(media = 0,
+
sigma = 1))
[1] "Normal"
La construcción representation("DistribucionAbsContinua", media =
"numeric", etc indica que la clase Normal“extiende”DistribucionAbsContinua,
ya que incorpora todas sus funcionalidades (en este primer ejemplo, bien pocas)
añadiendo otras nuevas.
Otra forma válida de indicar lo anterior serı́a mediante el argumento contains
de setClass:
> setClass("Normal", representation(media = "numeric", sigma = "numeric"),
+
prototype(media = 0, sigma = 1), contains = "DistribucionAbsContinua")
[1] "Normal"
Un ejemplo de lo anterior está en el siguiente código:
32
>
>
>
>
>
>
source("classesDistr2.r")
normal <- new("Normal")
normal2 <- new("Normal", media = -1, sigma = 3)
cauchy <- new("Cauchy")
gamm <- new("Gamma")
gamm2 <- new("Gamma", forma = 2, escala = 3)
Aparentemente poco ha cambiado, aunque algunas instrucciones, si las hubiésemos utilizado en secciones anteriores, darı́an una respuesta algo diferente:
> getAllSuperClasses(getClass("Gamma"))
[1] "DistribucionAbsContinua"
> superClassDepth(getClass("Normal"))
$label
[1] "DistribucionAbsContinua"
$depth
[1] 1
$ext
$ext$DistribucionAbsContinua
An object of class "SClassExtension"
Slot "subClass":
[1] "Normal"
Slot "superClass":
[1] "DistribucionAbsContinua"
Slot "package":
[1] ".GlobalEnv"
Slot "coerce":
function (from, strict = TRUE)
from
<environment: namespace:methods>
Slot "test":
function (object)
TRUE
<environment: namespace:methods>
Slot "replace":
function (from, to, value)
{
33
if (!is(value, "DistribucionAbsContinua"))
stop(gexttextf("the computation: as(object,\"%s\") <- value is valid when object has
"DistribucionAbsContinua", "Normal", "DistribucionAbsContinua",
class(value)), domain = NA)
value
}
Slot "simple":
[1] TRUE
Slot "by":
character(0)
Slot "dataPart":
[1] FALSE
Slot "distance":
[1] 1
> getClass("Cauchy")
Slots:
Name: mediana escala
Class: numeric numeric
Extends: "DistribucionAbsContinua"
> class(getClass("Cauchy"))
[1] "classRepresentation"
attr(,"package")
[1] "methods"
> isVirtualClass("DistribucionAbsContinua")
[1] TRUE
Instrucciones como las anteriores permiten cierta “introspección” en las clases
y sus instancias, lo que posibilita cierto grado de programación en el propio
lenguaje, como la creación de clases “sobre la marcha”. No profundizaremos en
este tema.
3.2
Clases virtuales
Tal como se ha podido comprobar mediante isVirtualClass, DistribucionAbsContinua
es una clase “abstracta” o “virtual”, como clase existe y puede tener interés como
34
generalización de otras (por ejemplo en el sentido que veremos luego), pero no
es instanciable, no existen objetos de dicha clase.
En el fichero classesDistr3.r tenemos un ejemplo más interesante del uso
de la herencia: La clase abstracta DistribucionAbsContinua posee un método que ya es operativo para calcular la función de distribución. Simplemente
implementa la definición general de función de distribución absolutamente continua. Por otra parte se ha añadido una nueva clase, Exponencial (también
especialización directa de DistribucionAbsContinua, decisión que revisaremos
más adelante), para la cual NO se ha definido el método distri:
> setClass("DistribucionAbsContinua", representation("VIRTUAL"))
[1] "DistribucionAbsContinua"
> setMethod("distri", signature("DistribucionAbsContinua"), function(distri,
+
x) {
+
f <- function(x) {
+
dens(distri, x)
+
}
+
integrate(f, -Inf, x)$value
+ })
[1] "distri"
> setClass("Exponencial", representation("DistribucionAbsContinua",
+
escala = "numeric"))
[1] "Exponencial"
> setValidity("Exponencial", function(object) {
+
if (object@escala <= 0)
+
return("Los parámetros de una Exponencial deben ser positivos")
+
else return(TRUE)
+ })
Slots:
Name:
escala
Class: numeric
Extends: "DistribucionAbsContinua"
> setMethod("dens", signature("Exponencial"), function(distri,
+
x) {
+
dexp(x, rate = 1/distri@escala)
+ })
[1] "dens"
35
> setMethod("cuantil", signature("Exponencial"), function(distri,
+
p) {
+
qexp(p, rate = 1/distri@escala)
+ })
[1] "cuantil"
> setMethod("genera", signature("Exponencial"), function(distri,
+
n = 1) {
+
rexp(n, rate = 1/distri@escala)
+ })
[1] "genera"
> setMethod("generaStd", signature("Exponencial"), function(distri,
+
n = 1) {
+
(rexp(n, rate = 1/distri@escala) - distri@escala)/distri@escala
+ })
[1] "generaStd"
En cambio vemos que dicho método funciona perfectamente para instancias
de esta clase, gracias a que lo ha “heredado” de DistribucionAbsContinua:
> source("classesDistr3.r")
> expo <- new("Exponencial", escala = 2)
> expo
An object of class "Exponencial"
Slot "escala":
[1] 2
> dens(expo, 2)
[1] 0.1839397
> distri(expo, 2)
[1] 0.6321206
> distri(expo, -1)
[1] 0
Lo anterior ilustra un aspecto fundamental de la OOP: el método distri
tiene signatura DistribucionAbsContinua. Es decir, se ha definido para esta
clase. Pero la llamada a la función genérica distri con un argumento de clase
Exponencial ha conducido a una llamada al método distri de DistribucionAbsContinua.
36
distri(expo, etc se ha “resuelto” (o “despachado”) mediante la búsqueda
de un método adecuado en la jerarquı́a de clases, desde clases más especializadas
a clases más generales. Como la clase Exponencial carecı́a de dicho método,
se ha activado el método del mismo nombre, el más especializado posible, que
se ha encontrado en la jerarquı́a de clases. En este caso el primero encontrado
ha sido el definido para DistribucionAbsContinua. Lógicamente, un método
distri propio de la clase Exponencial serı́a mucho más eficiente, al no tener
que recurrir a la integración numérica.
Nótese el empleo de la clase VIRTUAL en la definición de la clase Exponencial.
setClass("DistribucionAbsContinua", representation("VIRTUAL"))
Si se especifica que una clase desciende directamente de VIRTUAL se indica
explı́citamente que es virtual, es decir, que no es instanciable, no se pueden crear
objetos a partir de esta clase. En el ejemplo anterior no serı́a necesario emplear
VIRTUAL ya que una clase que no desciende explı́citamente de ninguna otra y
que tiene representation nula, es por defecto virtual. Pero resulta más claro
indicarlo de este modo.
3.3
Métodos de acceso a los slots
El siguiente código, aparentemente inocuo:
> gamm <- new("Gamma")
> gamm@escala <- 10
> gamm
An object of class "Gamma"
Slot "forma":
[1] 1
Slot "escala":
[1] 10
Slot "media":
[1] 1
Slot "sigma":
[1] 1
ha “destrozado” la estructura interna de gamm: a pesar de tener ahora parámetros
forma = 4 y escala = 10, su media y sigma siguen siendo 8 y 4. El comportamiento de este objeto será errático, como puede comprobarse, por ejemplo,
al hacerle generar valores según generaStd -y es probable que no nos demos
cuenta de nada.
La raı́z de este problema está en la imposibilidad de proteger u ocultar
slots, de controlar de alguna manera el acceso a los slots. (setClass posee
37
un argumento access, seguramente pensado para futuras extensiones en este
sentido del modelo de objetos S4, pero que de momento no se utiliza.) Por el
momento, sólo queda la alternativa de pedir, implorar, exigir... que no se acceda
directamente a los slots (mediante el operador @) y que al crear clases se definan
métodos de acceso a los slots, para que no tenga que hacerse directamente.
Dichos métodos se supone que velarán por mantener la coherencia interna de
los objetos.
En algunos lenguajes es práctica habitual designar estos métodos mediante
los prefijos “get” y “set”. Por ejemplo, en la clase Gamma, el método para conocer
el valor del slot forma se denominarı́a getForma y el método para modificarlo se
denominarı́a setForma. En S/R la tradición es designar estos métodos mediante
nombres como forma y forma<- respectivamente, es decir consultar el valor del
slot mediante el mismo nombre del slot (pero ahora empleado como una función)
y modificarlo mediante un método denominado mediante el propio nombre del
slot seguido de <-. Esta sintaxis permite que funcionen automáticamente las
tı́picas construcciones R del estilo forma(gamm) <- 2.0.
En el fichero classesDistr.r se han creado accesores para los slots de las
distintas clases. Primero se han definido las correspondientes funciones genéricas. Por ejemplo, las funciones genéricas relacionadas con la clase Gamma serı́an:
> setGeneric("forma", function(distri) standardGeneric("forma"))
[1] "forma"
> setGeneric("forma<-", function(distri, value) standardGeneric("forma<-"))
[1] "forma<-"
> setGeneric("escala", function(distri) standardGeneric("escala"))
[1] "escala"
> setGeneric("escala<-", function(distri, value) standardGeneric("escala<-"))
[1] "escala<-"
A continuación se definen los correspondientes métodos. La creación de los
métodos de consulta es trivial. La creación de los métodos de modificación suele
hacerse indirectamente, mediante una llamada a la función setReplaceMethod:
> setMethod("forma", signature("Gamma"), function(distri) return(distri@forma))
[1] "forma"
> setReplaceMethod("forma", signature("Gamma", "numeric"), function(distri,
+
value) {
+
if (value > 0) {
+
distri@forma <- value
+
distri@media <- value * distri@escala
38
+
+
+
+
+ })
distri@sigma <- sqrt(value) * distri@escala
}
else stop("El parámetro 'forma' de una Gamma debe ser positivo")
return(distri)
[1] "forma<-"
> setMethod("escala", signature("Gamma"), function(distri) return(distri@escala))
[1] "escala"
> setReplaceMethod("escala", signature("Gamma", "numeric"), function(distri,
+
value) {
+
if (value > 0) {
+
distri@escala <- value
+
distri@media <- distri@forma * value
+
distri@sigma <- sqrt(distri@forma) * value
+
}
+
else stop("El parámetro 'escala' de una Gamma debe ser positivo")
+
return(distri)
+ })
[1] "escala<-"
Ahora se puede modificar el valor de un campo de las instancias de Gamma,
de forma segura:
> gamm <- new("Gamma")
> gamm
An object of class "Gamma"
Slot "forma":
[1] 1
Slot "escala":
[1] 1
Slot "media":
[1] 1
Slot "sigma":
[1] 1
> escala(gamm) <- 10
> gamm
39
An object of class "Gamma"
Slot "forma":
[1] 1
Slot "escala":
[1] 10
Slot "media":
[1] 10
Slot "sigma":
[1] 10
> escala(gamm)
[1] 10
> forma(gamm) <- 4
> gamm
An object of class "Gamma"
Slot "forma":
[1] 4
Slot "escala":
[1] 10
Slot "media":
[1] 40
Slot "sigma":
[1] 20
3.4
Uso y abuso de la herencia
¿Debe Exponencial descender de Gamma? O por el contrario ¿Debe Gamma descender de Exponencial? ¿O ya está bien tal como lo hemos definido en los
ejemplos anteriores? Hay razones a favor y en contra de cada una de estas
opciones, pero lo que sigue apunta a que relacionar ambas clases mediante la
relación de herencia más bien es un ejemplo de mal uso de dicha relación.
A priori hay razones para considerar la herencia para relacionar ambas clases:
Una distribución exponencial de parámetro escala ES una Gamma de parámetros forma = 1 y escala. Por lo tanto la distribución exponencial se podrı́a
considerar una especialización de la Gamma. Pero lo anterior choca con la
manera en que habitualmente los lenguajes de programación (S/R incluido)
implementan la herencia. Exponencial heredará TODOS los slots de Gamma y
40
ahora el slot forma más bien sobra (por ejemplo, podrı́a dar problemas modificar
accidentalmente su valor a algo distinto de 1).
En vista de lo anterior, por el hecho de que una exponencial quedarı́a perfectamente definida por un solo slot, escala, y que para definir una Gamma
necesitarı́amos un slot adicional, forma, podrı́amos pensar en hacer descender la clase Gamma de Exponencial (que a su vez descenderı́a directamente de
DistribucionAbsContinua).
Esta ambigüedad procede de que la manera habitual de concebir la herencia
es una mezcla de dos cosas en realidad no coincidentes. Por un lado tenemos
el concepto abstracto de que “un A es un B” y por otro lado el hecho de que
“toda la información que definı́a B también va a definir A”. A veces más bien
quisiéramos eliminar información que poseı́a B.
Examinemos con más detalle las dos opciones planteadas para relacionar las
clases Gamma y Exponencial:
Definición de Exponencial como descendiente de Gamma:
El código completo de esta opción está definido en classesDistr2muchHerencia1.r.
Parece a priori una forma cómoda de especializar Gamma. Una razón para tomar
esta decisión de diseño podrı́a ser aprovechar la mayorı́a de métodos de Gamma,
que tendrı́an que seguir funcionando adecuadamente para “gammas de parámetro ‘forma’ igual a 1”. Parece que bastarı́a modificar la comprobación de validez
de las instancias y añadir posibles métodos más especı́ficos, como noMemoria.
Aparentemente todo funciona bien:
> source("classesDistr2muchHerencia1.r")
> expo <- new("Exponencial", escala = 2)
> expo
An object of class "Exponencial"
Slot "forma":
[1] 1
Slot "escala":
[1] 2
Slot "media":
[1] 2
Slot "sigma":
[1] 2
> dens(expo, 2)
[1] 0.1839397
> distri(expo, 2)
[1] 0.6321206
41
> distri(expo, -1)
[1] 0
> noMemoria(expo)
[1] "Cumple que Pr{X > t+x | X > t} = Pr{X > x} para t > 0, x > 0"
> expoTonta <- new("Exponencial", forma = 1, escala = 2)
> dens(expoTonta, 2)
[1] 0.1839397
> distri(expoTonta, 2)
[1] 0.6321206
> distri(expoTonta, -1)
[1] 0
> noMemoria(expoTonta)
[1] "Cumple que Pr{X > t+x | X > t} = Pr{X > x} para t > 0, x > 0"
> expoMala <- new("Exponencial", forma = 2)
Error in validObject(.Object) :
invalid class "Exponencial" object: Los parámetros de una Gamma deben ser positivos
> expoMuyMala <- new("Exponencial", forma = 2, escala = -2)
Error in validObject(.Object) :
invalid class "Exponencial" object: Los parámetros de una Gamma deben ser positivos
Pero hay riesgo de realizar cálculos erróneos y difı́ciles de detectar:
> forma(expo) <- 100
> distri(expo, 2)
[1] 3.981281e-159
Estos problemas se corregirı́an en gran parte (re)definiendo los correspondientes métodos (distri, etc) en la clase Exponencial. Basta quitar el código
comentado en classesDistr2muchHerencia1.r. Pero procediendo de esta manera no resulta evidente ninguna ventaja en hacer descender Exponencial de
Gamma, a no ser que complicar las cosas sea una “ventaja”. Muy poco código
de Gamma se puede reutilizar en Exponencial. Otra razón por la que el anterior diseño de clases es una mala opción es que no permite que un método de
Exponencial que se base o utilice una propiedad caracterı́stica de dicha distribución, se pueda utilizar también para una Gamma adecuada, es decir con
parámetro forma igual a 1:
42
> expo <- new("Exponencial", escala = 2)
> noMemoria(expo)
[1] "Cumple que Pr{X > t+x | X > t} = Pr{X > x} para t > 0, x > 0"
> gamm <- new("Gamma", forma = 1, escala = 2)
>noMemoria(gamm)
Error in function (classes, fdef, mtable) :
unable to find an inherited method for function "noMemoria", for signature "Gamma"
Definición de Gamma como descendiente de Exponencial
El fichero classesDistr2muchHerencia2.r contiene el código completo de
esta opción. Ya de entrada parece claro que no se consigue mucha reutilización
de código.
> source("classesDistr2muchHerencia2.r")
Otenemos un código algo más seguro:
> expoTonta <- new("Exponencial", forma = 1, escala = 2)
Error in .local(.Object, ...) : unused argument(s) (forma = 1)
Que admite un uso lógico del método ’noMemoria’
> expo <- new("Exponencial", escala = 2)
> noMemoria(expo)
[1] "Cumple que Pr{X > t+x | X > t} = Pr{X > x} para t > 0, x > 0"
> gamm <- new("Gamma", forma = 1, escala = 2)
> noMemoria(gamm)
[1] "Cumple que Pr{X > t+x | X > t} = Pr{X > x} para t > 0, x > 0"
Si bien también hace cosas inadecuadas:
> gamm <- new("Gamma", forma = 4, escala = 2)
> noMemoria(gamm)
[1] "Cumple que Pr{X > t+x | X > t} = Pr{X > x} para t > 0, x > 0"
El método ’noMemoria’ tendrı́a que ser exclusivamente de Exponencial aunque serı́a deseable que, bajo control, también tuviese sentido para ciertos objetos
de clase Gamma.
Una solución a lo anterior es el empleo de métodos ’as’ que realizan la “coerción” de un objeto “obligándolo” explı́citamente a adaptarse a otra clase. Los
= setAs( from =
métodos “as” se suelen definir mediante la función setAs:
”Gamma”, to = ”Exponencial”, def = function(from) if (from@forma != 1) stop(
”Sólo es posible convertir una Gamma de parámetro forma = 1 en Exponencial”)
new(”Exponencial”, escala = from@escala) )
El resultado de la anterior ejecución de setAs es la creación de un método
que permite forzar que una instancia de Gamma se convierta en una instancia de
Exponencial:
43
> source("classesDistr.r")
> extends("Gamma", "Exponencial")
[1] FALSE
> extends("Exponencial", "Gamma")
[1] FALSE
> gamm <- new("Gamma", forma = 1, escala = 2)
> inherits(gamm, "Gamma")
[1] TRUE
> inherits(gamm, "Exponencial")
[1] FALSE
> noMemoria(gamm)
Error in function (classes, fdef, mtable) :
unable to find an inherited method for function "noMemoria", for signature "Gamma"
> noMemoria(as(gamm, "Exponencial"))
[1] "Cumple que Pr{X > t+x | X > t} = Pr{X > x} para t > 0, x > 0"
> gamm <- new("Gamma", forma = 4, escala = 2)
> as(gamm, "Exponencial")
Error in asMethod(object) :
Sólo es posible convertir una Gamma de parámetro forma = 1 en Exponencial
De esta manera tenemos más bajo control la posible relación entre Gamma y
Exponencial.
44
Descargar