5. Fonctions définies par l'usager▲
Objectifs du chapitre |
---|
|
La possibilité pour l'usager de définir facilement et rapidement de nouvelles fonctions — et donc des extensions au langage — est une des grandes forces de R. Les fonctions personnelles définies dans l'espace de travail ou dans un package sont traitées par le système exactement comme les fonctions internes.
Ce court chapitre passe en revue la syntaxe et les règles pour créer des fonctions dans R. On discute également brièvement de débogage et de style de codage.
Énoncé du problème |
Stock Ticker est un jeu canadien datant de 1937 dans lequel on brasse une série de dés pour simuler les mouvements boursiers et les dividendes de six titres financiers.
La valeur de départ du titre est 100. Sélectionnez 1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12.
Écrire une fonction valeurs() servant à calculer les valeurs successives du titre. Sélectionnez
|
Le calcul de la valeur du titre étant récursif, l'utilisation d'une boucle est ici inévitable. Comme le nombre de répétitions est connu d'avance (le nombre de valeurs à calculer correspond au nombre de lancers de dés), une boucle for() serait le choix approprié.
5-1. Définition d'une fonction▲
On définit une nouvelle fonction avec la syntaxe suivante :
fun <-
function
(arguments) expression
où
- fun est le nom de la fonction (les règles pour les noms de fonctions étant les mêmes que celles présentées à la section 2.2Conventions pour les noms d'objets pour tout autre objet) ;
- arguments est la liste des arguments, séparés par des virgules ;
- expression constitue le corps de la fonction, soit une expression ou un groupe d'expressions réunies par des accolades.
5-2. Retourner des résultats▲
La plupart des fonctions sont écrites dans le but de retourner un résultat. Or, les règles d'interprétation d'un groupe d'expressions présentées à la section 2.1Commandes R s'appliquent ici au corps de la fonction.
- Une fonction retourne tout simplement le résultat de la dernière expression du corps de la fonction.
- On évitera donc que la dernière expression soit une affectation, car la fonction ne retournera alors rien et on ne pourra utiliser une construction de la forme x <- f() pour affecter le résultat de la fonction à une variable.
- Si on doit retourner un résultat sans être à la dernière ligne de la fonction (à l'intérieur d'un bloc conditionnel, par exemple), on utilise la fonction return. L'utilisation de return à la toute fin d'une fonction est tout à fait inutile et considérée comme du mauvais style en R.
- Lorsqu'une fonction doit retourner plusieurs résultats, il est en général préférable d'avoir recours à une liste nommée.
5-3. Variables locales et globales▲
Comme la majorité des langages de programmation, R comporte des concepts de variable locale et de variable globale.
-
Toute variable définie dans une fonction est locale à cette fonction, c'est-à-dire qu'elle :
- n'apparaît pas dans l'espace de travail ;
- n'écrase pas une variable du même nom dans l'espace de travail.
- Il est possible de définir une variable dans l'espace de travail depuis une fonction avec l'opérateur d'affectation
<<-
. Il est très rare — et généralement non recommandé — de devoir recourir à de telles variables globales. - On peut définir une fonction à l'intérieur d'une autre fonction. Cette fonction sera locale à la fonction dans laquelle elle est définie.
Le lecteur intéressé à en savoir plus pourra consulter les sections de la documentation de R portant sur la portée lexicale (lexical scoping). C'est un sujet important et intéressant, mais malheureusement trop avancé pour ce document d'introduction à la programmation en R.
5-4. Exemple de fonction▲
Le code développé pour l'exemple de point fixe de la section 4.4Algorithme du point fixe peut être intégré dans une fonction ; voir la figure 5.1.
- Le nom de la fonction est fp.
- La fonction compte quatre arguments : k, n, start et TOL.
- Les deux derniers arguments ont respectivement des valeurs par défaut de 0.05 et 10−10.
- La fonction retourne la valeur de la variable i puisque l'on évalue celle-ci à la dernière ligne (ou expression) de la fonction.
La définition de la fonction devra inclure start
=
100
comme deuxième argument afin de spécifier que :
- start est un argument de la fonction ;
- La valeur par défaut de l'argument est 100.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
fp <-
function
(k, n, start
=
0.05
, TOL =
1E-10
)
{
## Fonction pour trouver par la méthode du point
## fixe le taux d'intérêt pour lequel une série de
## 'n' paiements vaut 'k'.
##
## ARGUMENTS
##
## k: la valeur présente des paiements
## n: le nombre de paiements
## start: point de départ des itérations
## TOL: niveau de précision souhaité
##
## RETOURNE
##
## Le taux d'intérêt
i <-
start
repeat
{
it <-
i
i <-
(1
-
(1
+
it)^
(-
n))/
k
if
(abs
(i -
it)/
it <
TOL)
break
}
i
}
5-5. Fonctions anonymes▲
Il est parfois utile de définir une fonction sans lui attribuer un nom — d'où la notion de fonction anonyme. Il s'agira en général de fonctions courtes utilisées dans une autre fonction. Par exemple, pour calculer la valeur de xy2 pour toutes les combinaisons de x et y stockées dans des vecteurs du même nom, on pourrait utiliser la fonction outer ainsi :
2.
3.
4.
5.
6.
7.
>
x <-
1
:
3
; y <-
4
:
6
>
f <-
function
(x, y) x *
y^
2
>
outer
(x, y, f)
[
,1
]
[
,2
]
[
,3
]
[
1
,]
16
25
36
[
2
,]
32
50
72
[
3
,]
48
75
108
Cependant, si la fonction f ne sert à rien ultérieurement, on peut se contenter de passer l'objet fonction à outer sans jamais lui attribuer un nom :
2.
3.
4.
5.
>
outer
(x, y, function
(x, y) x *
y^
2
)
[
,1
]
[
,2
]
[
,3
]
[
1
,]
16
25
36
[
2
,]
32
50
72
[
3
,]
48
75
108
On a alors utilisé dans outer une fonction anonyme.
5-6. Débogage de fonctions▲
Il est assez rare d'arriver à écrire un bout de code sans bogue du premier coup. Par conséquent, qui dit programmation dit séances de débogage.
Les techniques de débogage les plus simples et naïves sont parfois les plus efficaces et certainement les plus faciles à apprendre. Loin d'un traité sur le débogage de code R, nous offrons seulement ici quelques trucs que nous utilisons régulièrement.
- Les erreurs de syntaxe sont les plus fréquentes (en particulier l'oubli de virgules). Lors de la définition d'une fonction, une vérification de la syntaxe est effectuée par l'interprète R. Attention, cependant : une erreur peut prendre sa source plusieurs lignes avant celle que l'interprète pointe comme causant problème.
-
Les messages d'erreur de l'interprète ne sont pas toujours d'un grand secours… tant que l'on n'a pas appris à les reconnaître. Un exemple de message d'erreur fréquemment rencontré :
-
valeur manquante là où TRUE / FALSE est requis
Cette erreur provient généralement d'une commande if dont l'argument vaut
NA
plutôt queTRUE
ouFALSE
. La raison : des valeurs manquantes se sont faufilées dans les calculs à notre insu jusqu'à l'instructionif
, faisant en sorte que l'argument deif
vautNA
alors qu'il ne peut être que booléen.
-
-
Lorsqu'une fonction ne retourne pas le résultat attendu, placer des commandes print à l'intérieur de la fonction, de façon à pouvoir suivre les valeurs prises par les différentes variables.
Par exemple, la modification suivante à la boucle de la fonction fp permet d'afficher les valeurs successives de la variable i et de détecter, par exemple, une procédure itérative divergente :
Sélectionnez1.
2.
3.
4.
5.
6.
7.
8.repeat
{
it<-
i i<-
(1
-
(1
+
it)^
(-
n))/
kprint
(i)if
(abs
((i-
it)/
it<
TOL))break
}
- Quand ce qui précède ne fonctionne pas, ne reste souvent qu'à exécuter manuellement la fonction. Pour ce faire, définir dans l'espace de travail tous les arguments de la fonction, puis exécuter le corps de la fonction ligne par ligne. La vérification du résultat de chaque ligne permet généralement de retrouver la ou les expressions qui causent problème.
5-7. Styles de codage▲
Si tous conviennent que l'adoption d'un style propre et uniforme favorise le développement et la lecture de code, il existe plusieurs chapelles dans le monde des programmeurs quant à la « bonne façon » de présenter et, surtout, d'indenter le code informatique.
Par exemple, Emacs reconnaît et supporte les styles de codage suivants, entre autres :
2.
3.
4.
for
(i in
1
:
10
)
{
expression
}
2.
3.
for
(i in
1
:
10
){
expression
}
2.
3.
4.
for
(i in
1
:
10
)
{
expression
}
2.
3.
4.
for
(i in
1
:
10
)
{
expression
}
- Pour des raisons générales de lisibilité et de popularité, le style C++, avec les accolades sur leurs propres lignes et une indentation de quatre (4) espaces est considéré comme standard pour la programmation en R.
- Consulter la documentation de votre éditeur de texte pour savoir s'il est possible de configurer le niveau d'indentation. La plupart des bons éditeurs pour programmeurs le permettent.
- Surtout, éviter de ne pas du tout indenter le code.
Solution du problème |
Il existe bien évidemment une multitude de solutions valides. Celle que nous proposons à la figure 5.2 repose sur deux idées principales :
Ensemble, ces deux stratégies permettent d'en arriver à une fonction compacte et efficace. |
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
valeurs <-
function
(x, start
=
100
)
{
## Création d'un vecteur pour les résultats. La valeur
## de départ est placée au début du vecteur pour faire
## la boucle. Elle sera supprimée à la fin.
res <-
c
(start
, numeric
(nrow
(x)))
## Conversion des étiquettes ("hausse", "nul", "baisse")
## de la première colonne des données en
## valeurs numériques (1, 0, -1).
d <-
(x[
, 1
]
==
"hausse"
) -
(x[
, 1
]
==
"baisse"
)
## Calcul des valeurs successives du titre.
for
(i in
seq
(length
(res) -
1
))
res[
i +
1
]
<-
res[
i]
+
d[
i]
*
x[
i, 2
]
## Résultats sans la valeur de départ
res[-
1
]
}
5-8. Exemples▲
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
125.
126.
127.
128.
129.
130.
131.
132.
133.
134.
135.
136.
137.
138.
139.
140.
141.
142.
143.
144.
145.
146.
147.
148.
149.
150.
151.
152.
153.
154.
155.
156.
157.
158.
### POINT FIXE
## Comme premier exemple de fonction, on réalise une mise en
## œuvre de l'algorithme du point fixe pour trouver le taux
## d'intérêt tel que a_angle{n} = k pour 'n' et 'k' donnés.
## Cette mise en œuvre est peu générale puisqu'il faudrait
## modifier la fonction chaque fois que l'on change la
## fonction f(x) dont on cherche le point fixe.
fp1 <-
function
(k, n, start
=
0.05
, TOL =
1E-10
)
{
i <-
start
repeat
{
it <-
i
i <-
(1
-
(1
+
it)^
(-
n))/
k
if
(abs
(i -
it)/
it <
TOL)
break
}
i
}
fp1(7.2
, 10
) # valeur de départ par défaut
fp1(7.2
, 10
, 0.06
) # valeur de départ spécifiée
i # les variables n'existent pas...
start
# ... dans l'espace de travail
## Généralisation de la fonction 'fp1': la fonction f(x) dont
## on cherche le point fixe (c'est-à-dire la valeur de 'x'
## tel que f(x) = x) est passée en argument. On peut faire
## ça ? Bien sûr, puisqu'une fonction est un objet comme un
## autre en R. On ajoute également à la fonction un argument
## 'echo' qui, lorsque TRUE, fera en sorte d'afficher à
## l'écran les valeurs successives de 'x'.
##
## Ci-dessous, il est implicite que le premier argument, FUN,
## est une fonction.
fp2 <-
function
(FUN, start
, echo =
FALSE
, TOL =
1E-10
)
{
x <-
start
repeat
{
xt <-
x
if
(echo) # inutile de faire 'if (echo == TRUE)'
print
(xt)
x <-
FUN(xt) # appel de la fonction
if
(abs
(x -
xt)/
xt <
TOL)
break
}
x
}
f <-
function
(i) (1
-
(1
+
i)^
(-
10
))/
7.2
# définition de f(x)
fp2(f, 0.05
) # solution
fp2(f, 0.05
, echo =
TRUE
) # avec résultats intermédiaires
fp2(function
(x) 3
^
(-
x), start
=
0.5
) # avec fonction anonyme
## Amélioration mineure à la fonction 'fp2': puisque la
## valeur de 'echo' ne change pas pendant l'exécution de la
## fonction, on peut éviter de refaire le test à chaque
## itération de la boucle. Une solution élégante consiste à
## utiliser un outil avancé du langage R : les expressions.
##
## L'objet créé par la fonction 'expression' est une
## expression non encore évaluée (comme si on n'avait pas
## appuyé sur Entrée à la fin de la ligne). On peut ensuite
## évaluer l'expression (appuyer sur Entrée) avec 'exec'.
fp3 <-
function
(FUN, start
, echo =
FALSE
, TOL =
1E-10
)
{
x <-
start
## Choisir l'expression à exécuter plus loin
if
(echo)
expr <-
expression
(print
(xt <-
x))
else
expr <-
expression
(xt <-
x)
repeat
{
eval
(expr) # évaluer l'expression
x <-
FUN(xt) # appel de la fonction
if
(abs
(x -
xt)/
xt <
TOL)
break
}
x
}
fp3(f, 0.05
, echo =
TRUE
) # avec résultats intermédiaires
fp3(function
(x) 3
^
(-
x), start
=
0.5
) # avec une fonction anonyme
### SUITE DE FIBONACCI
## On a présenté au chapitre 4 deux manières différentes de
## pour calculer les 'n' premières valeurs de la suite de
## Fibonacci. On crée d'abord des fonctions à partir de ce
## code. Avantage d'avoir des fonctions : elles sont valides
## pour tout 'n' > 2.
##
## D'abord la version inefficace parce qu'elle souffre du
## Syndrome de la plaque à biscuits décrit au chapitre 4.
fib1 <-
function
(n)
{
res <-
c
(0
, 1
)
for
(i in
3
:
n)
res[
i]
<-
res[
i -
1
]
+
res[
i -
2
]
res
}
fib1(10
)
fib1(20
)
## Puis la version qui devrait s'avérer plus efficace parce
## que l'on initialise d'entrée de jeu un contenant de la
## bonne longueur qu'on remplit par la suite.
fib2 <-
function
(n)
{
res <-
numeric
(n) # contenant créé
res[
2
]
<-
1
# res[1] vaut déjà 0
for
(i in
3
:
n)
res[
i]
<-
res[
i -
1
]
+
res[
i -
2
]
res
}
fib2(5
)
fib2(20
)
## A-t-on vraiment gagné en efficacité? Comparons le temps
## requis pour générer une longue suite de Fibonacci avec les
## deux fonctions.
system.time
(fib1(10000
)) # version inefficace
system.time
(fib2(10000
)) # version efficace, ~5x plus rapide
## Variation sur un même thème : une fonction pour calculer non
## pas les 'n' premières valeurs de la suite de Fibonacci,
## mais uniquement la 'n'ième valeur.
##
## Mais il y a un mais : la fonction 'fib3' est truffée
## d'erreurs (de syntaxe, d'algorithmique, de conception). À
## vous de trouver les bogues. (Afin de préserver cet
## exemple, copier le code erroné plus bas ou dans un autre
## fichier avant d'y faire les corrections.)
fib3 <-
function
(nb)
{
x <-
0
x1 _ 0
x2 <-
1
while
(n >
0
)
{
x <-
x1 +
x2
x2 <-
x1
x1 <-
x
n <-
n -
1
}
}
fib3(1
) # devrait donner 0
fib3(2
) # devrait donner 1
fib3(5
) # devrait donner 3
fib3(10
) # devrait donner 34
fib3(20
) # devrait donner 4181
5-9. Exercices▲
5.1 (solution) La fonction var calcule l'estimateur sans biais de la variance d'une population à partir de l'échantillon donné en argument. Écrire une fonction variance qui calculera l'estimateur biaisé ou sans biais selon que l'argument biased sera TRUE
ou FALSE
, respectivement. Le comportement par défaut de variance devrait être le même que celui de var. L'estimateur sans biais de la variance à partir d'un échantillon kitxmlcodeinlinelatexdvpX_1,\dots,X_nfinkitxmlcodeinlinelatexdvp est
alors que l'estimateur biaisé est
kitxmlcodelatexdvpS_n^2 = \frac{1}{n}\sum_{i=1}^n (X_i-\hat{X})^2finkitxmlcodelatexdvpoù kitxmlcodeinlinelatexdvp\hat{X} = n^{-1}(X_1+\cdots+X_n)finkitxmlcodeinlinelatexdvp.
5.2 (solution) Écrire une fonction matrix2 qui, contrairement à la fonction matrix, remplira par défaut la matrice par ligne. La fonction ne doit pas utiliser matrix. Les arguments de la fonction matrix2 seront les mêmes que ceux de matrix, sauf que l'argument byrow sera remplacé par bycol.
5.3 (solution) Écrire une fonction phi servant à calculer la fonction de densité de probabilité d'une loi normale centrée réduite, soit
kitxmlcodelatexdvp\phi(x) = \frac{1}{\sqrt{2\pi}}e^{-x^2/2},\ -\infty < x < \inftyfinkitxmlcodelatexdvpLa fonction devrait prendre en argument un vecteur de valeurs de x.Comparer les résultats avec ceux de la fonction dnorm.
5.4 (solution) Écrire une fonction Phi servant à calculer la fonction de répartition d'une loi normale centrée réduite, soit
kitxmlcodelatexdvp\Phi(x) = \int_{-\infty}^x \frac{1}{\sqrt{2\pi}}e^{-y^2/2}dy,\ -\infty<x<\inftyfinkitxmlcodelatexdvpSupposer, pour le moment, que kitxmlcodeinlinelatexdvpx\ge0finkitxmlcodeinlinelatexdvp. L'évaluation numérique de l'intégrale ci-dessus peut se faire avec l'identité
kitxmlcodelatexdvp\Phi(x) = \frac{1}{2}+\phi(x)\sum_{n=0}^\infty\frac{x^{2n+1}}{1\cdot3\cdot5\cdots(2n+1)},\ x\ge0finkitxmlcodelatexdvpUtiliser la fonction phi de l'exercice 5.3 et tronquer la somme infinie à une grande valeur, 50 par exemple. La fonction ne doit pas utiliser de boucles, mais peut ne prendre qu'une seule valeur de x à la fois. Comparer les résultats avec ceux de la fonction pnorm.
5.5 (solution) Modifier la fonction Phi de l'exercice 5.4 afin qu'elle admette des valeurs négatives. Lorsque kitxmlcodeinlinelatexdvpx<0,\ \Phi(x) = 1 - \Phi(-x)finkitxmlcodeinlinelatexdvp. La solution simple consiste à utiliser une structure de contrôle if
... else
, mais les curieux chercheront à s'en passer.
5.6 (solution) Généraliser maintenant la fonction de l'exercice 5.5 pour qu'elle prenne en argument un vecteur de valeurs de kitxmlcodeinlinelatexdvpxfinkitxmlcodeinlinelatexdvp. Ne pas utiliser de boucle. Comparer les résultats avec ceux de la fonction pnorm.
5.7 (solution) Sans utiliser l'opérateur %*%
, écrire une fonctionprod.mat qui effectuera le produit matriciel de deux matrices seulement si les dimensions de celles-ci le permettent. Cette fonction aura deux arguments (mat1 et mat2) et devra tout d'abord vérifier si le produit matriciel est possible. Si celui-ci est impossible, la fonction retourne un message d'erreur.
- Utiliser une structure de contrôle
if
...else
et deux boucles. - Utiliser une structure de contrôle
if
...else
et une seule boucle.
Dans chaque cas, comparer le résultat avec l'opérateur %*%
.
5.8 (solution) Vous devez calculer la note finale d'un groupe d'étudiants à partir de deux informations : 1) une matrice contenant la note sur 100 des étudiants à chacune des évaluations, et 2) un vecteur contenant la pondération des évaluations. Un de vos collègues a composé la fonction notes.finales ci-dessous afin de faire le calcul de la note finale pour chacun de ses étudiants. Votre collègue vous mentionne toutefois que sa fonction est plutôt lente et inefficace pour de grands groupes d'étudiants. Modifiez la fonction afin d'en réduire le nombre d'opérations et faire en sorte qu'elle n'utilise aucune boucle.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
notes.finales <-
function
(notes, p)
{
netud <-
nrow
(notes)
neval <-
ncol
(notes)
final <-
(1
:
netud) *
0
for
(i in
1
:
netud)
{
for
(j in
1
:
neval)
{
final[
i]
<-
final[
i]
+
notes[
i, j]
*
p[
j]
}
}
final
}
5.9 (solution) Trouver les erreurs qui empêchent la définition de la fonction ci-dessous.
2.
3.
4.
5.
6.
AnnuiteFinPeriode <-
function
(n, i)
{{
v <-
1
/
1
+
i)
ValPresChaquePmt <-
v^
(1
:
n)
sum
(ValPresChaquepmt)
}
5.10 (solution) La fonction ci-dessous calcule la valeur des paramètres d'une loi normale, gamma ou Pareto à partir de la moyenne et de la variance, qui sont connues par l'utilisateur.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
param <-
function
(moyenne, variance, loi)
{
loi <-
tolower
(loi) {
if
(loi ==
"normale"
)
param1 <-
moyenne
param2 <-
sqrt
(variance)
return
(list
(mean
=
param1, sd
=
param2))
}
if
(loi ==
"gamma"
) {
param2 <-
moyenne/
variance
param1 <-
moyenne *
param2
return
(list
(shape =
param1, scale
=
param2))
}
if
(loi ==
"pareto"
) {
cte <-
variance/
moyenne^
2
param1 <-
2
*
cte/
(cte-
1
)
param2 <-
moyenne *
(param1 -
1
)
return
(list
(alpha =
param1, lambda =
param2))
}
stop
("La loi doit être une de \"normale\", \"gamma\" ou \"pareto\""
)
}
L'utilisation de la fonction pour diverses lois donne les résultats suivants :
2.
3.
4.
5.
6.
7.
8.
9.
>
param(2
, 4
, "normale"
)
$
mean
[
1
]
2
$
sd
[
1
]
2
>
param(50
, 7500
, "gamma"
)
Erreur dans param(50
, 7500
, "gamma"
) :
Objet "param1"
introuvable
>
param(50
, 7500
, "pareto"
)
Erreur dans param(50
, 7500
, "pareto"
) :
Objet "param1"
introuvable
- Expliquer pour quelle raison la fonction se comporte ainsi.
- Appliquer les correctifs nécessaires à la fonction pour que celle-ci puisse calculer les bonnes valeurs. (Les erreurs ne se trouvent pas dans les mathématiques de la fonction.) Astuce : tirer profit du moteur d'indentation de votre éditeur de texte pour programmeur.