Annonce

Bienvenue sur le site support de mes ouvrages d'introduction à SAS

La 4ème édition de mon ouvrage est disponible depuis le 11 avril 2019 !

Où trouver cet ouvrage ?


#1 02-10-2019 07:34:25

SAS-SR
Administrateur
Lieu: Université d'Orléans
Date d'inscription: 01-09-2008
Site web

Saison 9 - les parrains et les filleuls

C'est la rentrée ! (oui... j'ai près d'un mois de retard...)

Remettons nous à programmer sous SAS et quoi de mieux qu'un exercice tiré de la "vie réelle" pour bien se remettre en jambe !

reprenons : la rentrée, c'était il y a près d'un mois et pour accueillir au mieux les étudiants qui arrivent en M1 ESA (48 étudiants), 26 étudiants volontaires du M2 ESA ont accepté de parrainer chacun jusqu'à deux étudiants.

Trois M2 ont rédigé un programme SAS afin de déterminer qui parrainera qui (bien entendu, il faut laisser faire le hasard) et ils sont venus me voir hier pour me montrer leur programme en me disant que ça ferait un sujet amusant pour les beaux mercredis - ils ont eu raison - qu'ils soient ici publiquement remerciés !

mais... ils ont commencé à m'expliquer leur programme et, assez rapidement, je leur ai dit que cela avait l'air bien compliqué... ils m'ont parlé de plein de tables créées, d'un programme macro... et je leur ai dit, qu'une fois qu'on disposait de la table contenant les prénoms des M1 et de celle contenant les prénoms des M2, une étape DATA devait suffire pour construire les couples parrain / filleul.

bon... réflexion faite, une étape data doit certes suffire mais elle ne s'annonce pas simple du tout en fait...

Alors on va procéder de la façon suivante : pour la semaine prochaine, partant des deux tables M1 et M2, vous avez droit à une procédure (je vous laisse choisir cette procédure) et une étape data pour arriver au résultat souhaité. (on verra ensuite si on peut vraiment obtenir un résultat correct avec une seule étape data...)

ce programme crée la table M2 :

Code:

data M2;
   input etu_m2 :$12. @@;
cards;
Marie Jean Jeanne Louis Marguerite Pierre Joseph Germaine Marcel Henri Louise André Georges Yvonne 
Madeleine René Paul Suzanne Charles François Maurice Emile Marcelle Marthe Maria Albert 
;

source des prénoms ? les prénoms les plus donnés en... 1901

ce programme crée la table M1 :

Code:

data M1;
   input etu_m1 :$12. @@;
cards;
camille louise léa ambre agathe jade julia mila alice chloé emma andréa anna lucie eden romane élise 
lola zoé emy léonie mia rose louis gabriel léo maël paul hugo valentin gabin arthur théo jules
lucas sacha ethan timéo antoine nathan raphaël thomas tom mathéo mathis samuel
;

source des prénoms ? top 50 des prénoms pour 2019

Tous les M2 volontaires doivent avoir au moins un filleul.
Tous les M1 doivent avoir un parrain.

dans le sujet en ligne entre le 2 octobre et le 3 octobre (10h14), il y avait une petite erreur : la table M1 contenant 48 prénoms alors qu'elle ne doit contenir que 46 prénoms. Le programme qui construit la table M1 vient d'être modifié : suppression de tiago et de candice - j'espère qu'ils me pardonneront...


amusez vous bien et à la semaine prochaine

Dernière modification par SAS-SR (03-10-2019 08:14:52)

Hors ligne

 

#2 09-10-2019 09:10:09

SAS-SR
Administrateur
Lieu: Université d'Orléans
Date d'inscription: 01-09-2008
Site web

Re: Saison 9 - les parrains et les filleuls

Organisons ce parrainage....

j'ai reçu deux très intéressantes propositions de la part de Kevin et de Songui mais elles ne répondaient pas à ma demande qui était "tout doit être fixé par le hasard".

Une façon de vérifier si tout est fixé par le hasard, c'est d'exécuter plusieurs fois le programme et vérifier que les résultats obtenus diffèrent totalement. Par exemple, ce ne doit pas être toujours les mêmes étudiants de M2 qui n'ont qu'un étudiant à parrainer (et les solutions de Songui et Kevin ne présentaient pas cette caractéristique).

De plus, ils utilisaient tous les deux PROC SURVEYSELECT (ce qui n'est pas une mauvais idée en soit mais comme je n'en parle pas dans mon bouquin...).

alors examinons une première solution qui n'utilise (presque...) que des outils de programmation présentés dans mon bouquin...

on va commencer par ça :

Code:

proc sql noprint;
   select etu_m2 format=$quote15. into :listem2 separated by ' ' from m2;
   select etu_m1 format=$quote15. into :listem1 separated by ' ' from m1;
run;

et là, mes étudiants en M1 râlent en disant "mais on n'a pas encore vu PROC SLQ !! quelle est l'idée de ce programme ???"

ce programme a pour objet de créer deux macro-variables (et mes étudiants en M1 râlent encore parce le langage macro n'a pas encore été vu) dans lesquels je vais stocker les prénoms de nos étudiants.

Voici la valeur de la macro variable LISTEM2 :

Code:

601  %put &listem2;
"Marie" "Jean" "Jeanne" "Louis" "Marguerite" "Pierre" "Joseph" "Germaine" "Marcel" "Henri" "Louise"
"André" "Georges" "Yvonne" "Madeleine" "René" "Paul" "Suzanne" "Charles" "François" "Maurice"
"Emile" "Marcelle" "Marthe" "Maria" "Albert"

grâce au format $QUOTE15., ces prénoms sont présentés entre quotes.

que va-ton faire de ces macro-variables ?

et bien les utiliser !

Code:

data toto;
   array p(26) $ 15 p1-p26 (&listem2);
   array f(46) $ 15 f1-f46 (&listem1);
...

je crée deux tableaux de variables : les variables de ces deux tableaux sont caractères, de longueur 15. Le tableau P contient 26 variables qui ont donc pour valeur les prénoms de mes étudiants en M2 (mes parrains). Le tableau F regroupe 46 variables qui ont pour valeur les prénoms des filleuls (si vous avez du mal à suivre, relisez attentivement les pages 208 à 211, ED4).

Le hasard doit décider de tout ?

Il nous faut donc "mélanger" les valeurs des variables au sein de chaque tableau.

comment faire ?

Et bien il faut se souvenir qu'un jour, nous avons joué à une jeu de la française des jeux (Amigo ! Tu veux jouer avec moi !) ainsi qu'au poker (Jouons au poker avec SAS !) et qu'on avait alors utilisé la routine CALL RANPERM.

RANPERM ? c'est pour RANdom PERMmutation

Notre programme devient :

Code:

data toto;
   array p(26) $ 15 p1-p26 (&listem2);
   array f(46) $ 15 f1-f46 (&listem1);
   seed=0;
   call ranperm(seed, of p:);
   call ranperm(seed, of f:);
...

le premier argument de la routine doit être une variable dont la valeur sera utilisée comme graine par le générateur de nombre aléatoire - il faut, au préalable, donner une valeur à cette variable. En donnant une valeur 0, nous demandons que l'horloge interne (i.e. le nombre de secondes depuis le 1er janvier 1960 à 0:00:00 au moment où vous demandez l'exécution de votre programme) soit utilisée comme graine.

conclusion : les permutations de modalités effectuées par CALL RANPERM seront (normalement) toujours différentes.

petite démonstration du fonctionnement de CALL RANPERM :

Code:

data test;
array X(10) (1 2 3 4 5 6 7 8 9 10);
seed=0;
call ranperm (seed,of x:);
run;

proc print;run;

à la première exécution du programme, j'ai obtenu ce résultat :

Code:

Obs.    X1    X2    X3    X4    X5    X6    X7    X8    X9    X10       seed

  1     10     8     7     5     2     9     4     3     1     6     2126329085

et à la seconde, ce résultat :

Code:

Obs.    X1    X2    X3    X4    X5    X6    X7    X8    X9    X10       seed

  1      1     7     5     8    10     4     6     2     9     3     1269069905

Dans notre petit exercice, CALL RANPERM va permettre de mélanger d'une part les noms des parrains et d'autre part, les noms des filleuls.

Nous n'avons plus qu'à attribuer à chaque parrains son ou ses filleuls.

26 parrains potentiels, 46 filleuls : 20 parrains auront deux filleuls et 6 parrains n'en auront qu'un.

le programme complet :

Code:

data toto;
   array p(26) $ 15 p1-p26 (&listem2);
   array f(46) $ 15 f1-f46 (&listem1);
   seed=0;
   call ranperm(seed, of p:);
   call ranperm(seed, of f:);
   do i=1 to 20 ;
      ensemble=compbl(p(i) || " : " || f(i) || " et " || f(i+20));
      output;
   end;
   do i=21 to 26;
      ensemble=compbl(p(i)||" : "||f(i+20));
      output;
   end;
   keep ensemble;
run;

Exécutez le programme plusieurs fois et vous contacterez bien que les associations parrain - filleul(s) sont systématiquement différentes.

(et prenez aussi le temps de bien comprendre le pourquoi des deux boucles DO...).

La semaine prochaine, nous examinerons une solution pour laquelle on ne mobilisera pas PROC SQL (ni aucune autre procédure d'ailleurs). Une fois les tables M1 et M2 construites, nous n'aurons besoin que d'une seule étape data pour créer nos associations parrain-filleul(s).

je vais vous aiguiller un peu de façon à ce que vous trouviez vous même la solution : le programme est quasiment identique à celui proposé aujourd'hui... je vais juste introduire quelques instructions (10 pour être précis - 10=2*5) après les instructions ARRAY et avant l'instruction qui définit la valeur de la variable SEED...

à la semaine prochaine

Hors ligne

 

#3 16-10-2019 12:23:00

SAS-SR
Administrateur
Lieu: Université d'Orléans
Date d'inscription: 01-09-2008
Site web

Re: Saison 9 - les parrains et les filleuls

Concluons...

je vous ai indiqué qu'au moyen d'une seule étape DATA, on pouvait arriver au résultat et qu'il fallait "simplement" ajouter 2*5 = 10 instructions entre les instructions ARRAY et l'instruction qui définit la valeur de SEED.

Nous partons donc de ce programme :

Code:

data toto2(keep=ensemble);
   array p(26) $ 15 p1-p26;
   array f(46) $ 15 f1-f46;

** 10 instructions à placer ici... ;

   seed=0;
   call ranperm(seed, of p:);
   call ranperm(seed, of f:);
   do i=1 to 20 ;
      ensemble=compbl(p(i) || " : " || f(i) || " et " || f(i+20));
      output;
   end;
   do i=21 to 26;
      ensemble=compbl(p(i)||" : "||f(i+20));
      output;
   end; 
run;

Evidemment, il faut déjà retirer l'appel aux macro-variables &listem1 et &listem2 mais comme le reste du programme est parfaitement identique à celui proposé la semaine dernière, vous devinez que nos 10 instructions auront pour objectif de donner leurs valeurs aux variables des deux tableaux.

comment allons-nous faire... ?

Code:

   do until (last1=1);
     obsm1+1;
     set m1 end=last1;
     f(obsm1)=etu_m1;
   end;

   do until (last2=1);
     obsm2+1;
     set m2 end=last2;
     p(obsm2)=etu_m2;
   end;

nous avons une première boucle DO UNTIL qui tournera jusqu'à ce que LAST1 soit égal à 1. LAST1 n'est pas une variable mais le marqueur associé à la dernière observation de la table M1. Notre boucle va donc tourner jusqu'à ce que l'on ait examiné l'ensemble des observations de la table M1.

Vous avez ensuite une variable OBSM1 qui augmentera de 1 à chaque tour de boucle. Au premier tour, OBSM1 est égal à 1, via SET, on demande l'ouverture de la table M1, la première observation (camille) arrive dans le PDV, la première variable du tableau F aura pour modalité "camille" (F(1)=ETU_M1).

cette première observation n'est pas la dernière, on remonte donc dans la boucle, OBSM1 augmente de 1 et vaut maintenant 2, on réouvre alors la table M1 et arrive dans le PDV la seconde observation de la table M1 (louise). le seconde variable du tableau F aura pour modalité "louise".

et voilà !

c'était facile ! (en tout cas, c'est ce que vous vous direz quand vous aurez lu la suite...)



J'ai reçu dans la semaine un programme rédigé par Victor et Thibaut qui construit aussi les couples parrain filleul en une étape data. Je leur ai dit que je posterai leur programme et l'expliquerai...

j'ai fait une sorte de promesse... alors il va falloir que j'explique leur infernal programme (parce que oui, disons le tout de suite, il est infernal...)

Code:

data titi (keep=etu_m1 etu_m2); 
/* initialisation du array pour les M1 */
count=0; 
array obsnum(46) _temporary_; 
/* initialisation du array pour les M2 (premier tour) */
comptage=0;
array obs(26) _temporary_;
/* initialisation du array pour les M2 (deuxième tour) */
comptage2=0;
array obs2(26) _temporary_;

do i=1 to 46;

/* Tirage aléatoire des M1 */
        redo: 
         select=ceil(ranuni(0)*46); 
          set m1 point=select ; 
            do j=1 to count; 
            if obsnum(j)=select then goto redo; 
        end; 
          position=select; 
          count=count+1; 
          obsnum(count)=select;

/* Tirage aléatoire des M2 */
/* Chaque M2 aura au moins un filleul */
    if count<27 then do;   
    st:
    rando=ceil(ranuni(0)*26);
    set m2 point=rando; 
        do j=1 to comptage;
        if obs(j)=rando then goto st;
        end;
    comptage=comptage+1;
    obs(comptage)=rando;
    output;
    end;

/* 20 M2 auront deux filleuls et 6 M2 auront un filleul */

    else do;
    star:
    randomy=ceil(ranuni(0)*26);
    set m2 point=randomy; 
       do j=1 to comptage2;
          if obs2(j)=randomy then goto star;
       end;
       comptage2=comptage2+1;
       obs2(comptage2)=randomy;
       output;

    end;
end;
stop;
run; 

proc sort data=titi; by etu_m2; run;

proc print data=titi; run;

je vous avais prévenu....

on commence par la définition de trois tableaux temporaires OBSNUM, OBS et OBS2 et la construction de trois variables de comptage COUNT, COMPTAGE et COMPTAGE2, initialisées à 0 (dont on va voir l'utilité...)

la boucle sur i vous indique que ce qui suit va être fait 46 fois, soit autant de fois qu'on a d'étudiants en M1.

premier ensemble à expliquer :

Code:

        redo: 
         select=ceil(ranuni(0)*46); 
          set m1 point=select ; 
            do j=1 to count; 
            if obsnum(j)=select then goto redo; 
        end; 
          count=count+1; 
          obsnum(count)=select;

ahhhh du GOTO !

l'idée est la suivante : on tire une valeur aléatoire SELECT entre 1 et 46 (select=ceil(ranuni(0)*46) - admettons que ce soit 23
on ouvre enusite la table M1 et grâce à l'option POINT= de SET (page 196 ED4), on accède directement à la 23ème observation (le 23ème prénom des M1 - rose - est donc présent dans le PDV).

la boucle DO sur J va nous permettre de vérifier si, dans le tableau OBSNUM, une des variables de ce tableau a pour valeur 23. Si c'est le cas, GOTO redo : on remonte juste après le mot clé REDO: et on tire une nouvelle valeur pour SELECT. Ce que l'on souhaite ici, c'est s'assurer qu'un étudiant M1 ne sera tiré qu'une fois.

petit exemple pour comprendre le fonctionnement et l'intérêt de ce GOTO :

Code:

data bidon(keep=x1-x10);
   array x(10);
   do i=1 to 10 ;
      redo:
      valeur=ceil(ranuni(0)*10);
      do j=1 to 10;
         if x(j)=valeur then goto redo;
      end;
      x(i)=valeur;
   end;
run;

je souhaite que les variables X1-X10 prennent des valeurs comprises entre 1 et 10 sans remise. Si une variable a pour valeur 1, aucune autre variable n'aura comme valeur 1.
Vous devez aussi comprendre que SAS peut tirer des valeurs aléatoirement un très grand nombre de fois jusqu'à ce qu'il trouve une valeur non encore attribuée !

modifions notre programme en introduisant deux instructions qui vont permettre de voir combien il a fait de tirages avant de trouver une valeur non encore attribuée :

Code:

data bidon(keep=x1-x100 iter:);
   array x(100);
   array iter(100);
   do i=1 to 100 ;
      redo:
      iter(i)+1;
      valeur=ceil(ranuni(0)*100);
      do j=1 to 100;
         if x(j)=valeur then goto redo;
      end;
      x(i)=valeur;
   end;
run;

et les valeurs des variables ITER80 à ITER100 sont :

Code:

                                                                                     i
i   i   i   i   i   i   i   i   i   i   i   i   i   i   i    i    i    i   i    i    t
t   t   t   t   t   t   t   t   t   t   t   t   t   t   t    t    t    t   t    t    e
e   e   e   e   e   e   e   e   e   e   e   e   e   e   e    e    e    e   e    e    r
r   r   r   r   r   r   r   r   r   r   r   r   r   r   r    r    r    r   r    r    1
8   8   8   8   8   8   8   8   8   8   9   9   9   9   9    9    9    9   9    9    0
0   1   2   3   4   5   6   7   8   9   0   1   2   3   4    5    6    7   8    9    0

4   4   2   3   2   4   1   4   5   9   5   4   2   7   1   25   102   9   6   24   53

pour trouver la 95ème valeur non encore attribuée, SAS a dû faire 25 tirages, 102 tirages pour trouver la 96ème valeur, 9 (le bol !) pour la 97ème valeur etc. etc.

vous avez peut être l'impression qu'on fait faire à SAS beaucoup de tirages et que le programme ne va pas être rapide... je vous rassure tout se fait en quelques centièmes de seconde dans notre exemple... évidemment, si au lieu de tirer entre 1 et 100, vous tirez entre 1 et 100000, vous allez avoir des soucis...

bref...

reprenons et expliquons le 2ème lot :

Code:

   if count<27 then do;   
      st:
      rando=ceil(ranuni(0)*26);
      set m2 point=rando; 
         do j=1 to comptage;
            if obs(j)=rando then goto st;
         end;
         comptage=comptage+1;
         obs(comptage)=rando;
         output;
   end;

la variable COUNT mesure le nombre de M1 qu'on a traité. Le second lot s'applique uniquement si strictement moins de 27 M1 ont été traité : vous avez 26 M2 et l'objet de ce programme va être s'attribuer à chaque M2 un M1.

On se souvient qu'à ce stade, dans le PDV, on a le nom d'un M1 (rose). Pour sélectionner un M2, on va procéder de la même manière que pour la sélection d'un M1. On tire une valeur au hasard comprise entre 1 et 26 (disons qu'on trouve 14), on ouvre alors la table M2 pour se rendre directement à la 14ème observation. Si la valeur 14 n'est pas parmi les valeurs des variables du tableau OBS, alors on a notre M2 dans le PDV (si non, on retire un chiffre entre 1 et 26).

Puisqu'on dispose d'un M1 et puisqu'on dispose d'un M2, on a un coupe parrain-filleul : on peut écrire une observation d'où l'instruction OUTPUT.

le troisième lot :

Code:

   else do;
     star:
     randomy=ceil(ranuni(0)*26);
     set m2 point=randomy; 
        do j=1 to comptage2;
           if obs2(j)=randomy then goto star;
        end;
        comptage2=comptage2+1;
        obs2(comptage2)=randomy;
        output;
   end;

le ELSE DO est liée à la condition d'exécution du 2ème lot. pour que le troisième lot s'exécute, il faut que COUNT soit supérieur ou égal à 27 (et que par conséquent, tous les M2 aient déjà un filleul). Il nous reste des M1 à ce stade et il va aussi falloir leur attribuer un parrain. La méthode de détermination du parrain est identique aux méthodes de sélections (lot 1 et lot 2) vu précédemment.

attention, il faut clore le programme :

Code:

end;
stop;
run;

Le END clos la boucle du i : les 46 M1 ont été traités

puisque vous avez utilisé de l'accès direct aux données via POINT=, l'instruction STOP est Obligatoire

et ENFIN, RUN...


et certains disent que mes programmes sont compliqués....

j'espère que vous êtes maintenant convaincu du contraire !

Merci à Victor et Thibaut pour leur programme ;-)

(et bonne chance pour la certification Advanced programming que vous passerez prochainement...)

Ce sujet est maintenant terminé - à la semaine prochaine pour un nouveau sujet des beaux mercredis

Hors ligne

 

Pied de page des forums

Propulsé par FluxBB
Traduction par FluxBB.fr
Flux RSS