Pour cette troisième note “rapide” sur la méthode Difference-in-Difference, nous allons considérer deux exemples tirés du cours d’introduction aux méthodes causales proposé par A. Colin Cameron de l’université de Californie-Davis (que vous trouverez ici). Ils sont initialement développés à partir de Stata. Nous nous efforcerons à la fois de les reproduire sous R et d’approfondir différents concepts. Mais avant d’aller plus loin, comme à chaque fois, chargeons les packages que nous allons utiliser: le tidyverse, que l’on ne présente plus, et haven, qui permet de lire et écrire des fichiers notamment au format .dta (le format de Stata), labbeled, qui permet de travailler plus facilement les données à étiquette (comme celles issues de Stata), lmtest et sandwich, qui permettent de réaliser des tests spécifiques sur les coefficients de régression.
# packages de gestion des données
library(tidyverse)
library(haven)
library(labelled)
# packages de regression et tests
library(fixest)
library(lmtest)
library(sandwich)
library(car)
2x2 Difference in Difference
Ceci fait, abordons la première partie. L’ensemble est tiré de Tanaka (2014). Dans ce papier, l’auteur se pose la question de savoir si un meilleur accès aux soins améliore la santé. Plus précisément si la gratuité des soins à un impacte positif sur le statut nutritionnel des enfants pauvres. Pour ce faire, il prend la cas de l’Afrique du sud post-Apartheid. Cela leur permet d’exploiter une variation exogène de la tarification des services de santé, le passage à la gratuité, du fait des différences marquées concernant l’implantation des cliniques sur le territoire héritées du régime discriminatoire qui régnait dans le pays avant 1991. En 1994, le gouvernement rend gratuit les services de santé pour les enfants de moins de 6 ans. Il s’agira ici de la date de traitement. Le changement tarifaire augmente en principe l’accès aux soins néanmoins pour que le changement soit effectif il faut que les ménages pauvres résident dans une région avec une clinique. Les enfants de ménages situés dans de telles région seront donc le groupé traité. Dans le cas contraire, si le ménage est situé dans une région sans clinique, la gratuité ne sert à rien puisque l’accès matériel aux soins reste très difficile. Les enfants de ces ménages seront le groupe de contrôle.
Chargement et exploration des données
Chargeons donc les données. Celles-ci sont disponibles ici. Le fichier est un .dta. Utilisons la fonction read correspondant dans le package haven.
dat<-read_dta("AED_HEALTHACCESS.dta")
Voyons ce que l’on a. Commençons dénombrer les variables et les observations.
c(nb_obs=nrow(dat),nb_var=length(dat))
## nb_obs nb_var
## 1071 26
La base obtenue comprend 1071 observations pour 26 variables, mais qu’elles sont-elles? Un des avantages des bases de données issues de logiciels comme Stata est qu’ils attachent différents attributs aux objets qu’ils traitent. Parmi eux, on trouve notamment des labels contenant des courts textes descriptifs utilisés notamment pour décrire les variables (ce qui peut être utile).
Pour obtenir, la liste des attributs d’un objet. Il suffit d’utiliser le fonction attributes(). Utilisons la sur dat notre base de données.
dat %>% attributes()
## $class
## [1] "tbl_df" "tbl" "data.frame"
##
## $row.names
## [1] 1 2 3 4 5 6 7 8 9 10 11 12 13 14
## [15] 15 16 17 18 19 20 21 22 23 24 25 26 27 28
## [29] 29 30 31 32 33 34 35 36 37 38 39 40 41 42
## [43] 43 44 45 46 47 48 49 50 51 52 53 54 55 56
## [57] 57 58 59 60 61 62 63 64 65 66 67 68 69 70
## [71] 71 72 73 74 75 76 77 78 79 80 81 82 83 84
## [85] 85 86 87 88 89 90 91 92 93 94 95 96 97 98
## [99] 99 100 101 102 103 104 105 106 107 108 109 110 111 112
## [113] 113 114 115 116 117 118 119 120 121 122 123 124 125 126
## [127] 127 128 129 130 131 132 133 134 135 136 137 138 139 140
## [141] 141 142 143 144 145 146 147 148 149 150 151 152 153 154
## [155] 155 156 157 158 159 160 161 162 163 164 165 166 167 168
## [169] 169 170 171 172 173 174 175 176 177 178 179 180 181 182
## [183] 183 184 185 186 187 188 189 190 191 192 193 194 195 196
## [197] 197 198 199 200 201 202 203 204 205 206 207 208 209 210
## [211] 211 212 213 214 215 216 217 218 219 220 221 222 223 224
## [225] 225 226 227 228 229 230 231 232 233 234 235 236 237 238
## [239] 239 240 241 242 243 244 245 246 247 248 249 250 251 252
## [253] 253 254 255 256 257 258 259 260 261 262 263 264 265 266
## [267] 267 268 269 270 271 272 273 274 275 276 277 278 279 280
## [281] 281 282 283 284 285 286 287 288 289 290 291 292 293 294
## [295] 295 296 297 298 299 300 301 302 303 304 305 306 307 308
## [309] 309 310 311 312 313 314 315 316 317 318 319 320 321 322
## [323] 323 324 325 326 327 328 329 330 331 332 333 334 335 336
## [337] 337 338 339 340 341 342 343 344 345 346 347 348 349 350
## [351] 351 352 353 354 355 356 357 358 359 360 361 362 363 364
## [365] 365 366 367 368 369 370 371 372 373 374 375 376 377 378
## [379] 379 380 381 382 383 384 385 386 387 388 389 390 391 392
## [393] 393 394 395 396 397 398 399 400 401 402 403 404 405 406
## [407] 407 408 409 410 411 412 413 414 415 416 417 418 419 420
## [421] 421 422 423 424 425 426 427 428 429 430 431 432 433 434
## [435] 435 436 437 438 439 440 441 442 443 444 445 446 447 448
## [449] 449 450 451 452 453 454 455 456 457 458 459 460 461 462
## [463] 463 464 465 466 467 468 469 470 471 472 473 474 475 476
## [477] 477 478 479 480 481 482 483 484 485 486 487 488 489 490
## [491] 491 492 493 494 495 496 497 498 499 500 501 502 503 504
## [505] 505 506 507 508 509 510 511 512 513 514 515 516 517 518
## [519] 519 520 521 522 523 524 525 526 527 528 529 530 531 532
## [533] 533 534 535 536 537 538 539 540 541 542 543 544 545 546
## [547] 547 548 549 550 551 552 553 554 555 556 557 558 559 560
## [561] 561 562 563 564 565 566 567 568 569 570 571 572 573 574
## [575] 575 576 577 578 579 580 581 582 583 584 585 586 587 588
## [589] 589 590 591 592 593 594 595 596 597 598 599 600 601 602
## [603] 603 604 605 606 607 608 609 610 611 612 613 614 615 616
## [617] 617 618 619 620 621 622 623 624 625 626 627 628 629 630
## [631] 631 632 633 634 635 636 637 638 639 640 641 642 643 644
## [645] 645 646 647 648 649 650 651 652 653 654 655 656 657 658
## [659] 659 660 661 662 663 664 665 666 667 668 669 670 671 672
## [673] 673 674 675 676 677 678 679 680 681 682 683 684 685 686
## [687] 687 688 689 690 691 692 693 694 695 696 697 698 699 700
## [701] 701 702 703 704 705 706 707 708 709 710 711 712 713 714
## [715] 715 716 717 718 719 720 721 722 723 724 725 726 727 728
## [729] 729 730 731 732 733 734 735 736 737 738 739 740 741 742
## [743] 743 744 745 746 747 748 749 750 751 752 753 754 755 756
## [757] 757 758 759 760 761 762 763 764 765 766 767 768 769 770
## [771] 771 772 773 774 775 776 777 778 779 780 781 782 783 784
## [785] 785 786 787 788 789 790 791 792 793 794 795 796 797 798
## [799] 799 800 801 802 803 804 805 806 807 808 809 810 811 812
## [813] 813 814 815 816 817 818 819 820 821 822 823 824 825 826
## [827] 827 828 829 830 831 832 833 834 835 836 837 838 839 840
## [841] 841 842 843 844 845 846 847 848 849 850 851 852 853 854
## [855] 855 856 857 858 859 860 861 862 863 864 865 866 867 868
## [869] 869 870 871 872 873 874 875 876 877 878 879 880 881 882
## [883] 883 884 885 886 887 888 889 890 891 892 893 894 895 896
## [897] 897 898 899 900 901 902 903 904 905 906 907 908 909 910
## [911] 911 912 913 914 915 916 917 918 919 920 921 922 923 924
## [925] 925 926 927 928 929 930 931 932 933 934 935 936 937 938
## [939] 939 940 941 942 943 944 945 946 947 948 949 950 951 952
## [953] 953 954 955 956 957 958 959 960 961 962 963 964 965 966
## [967] 967 968 969 970 971 972 973 974 975 976 977 978 979 980
## [981] 981 982 983 984 985 986 987 988 989 990 991 992 993 994
## [995] 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008
## [1009] 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022
## [1023] 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036
## [1037] 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050
## [1051] 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064
## [1065] 1065 1066 1067 1068 1069 1070 1071
##
## $label
## [1] "Data for A. Colin Cameron (2022), ANALYSIS OF ECONOMIC DATA, Amazon"
##
## $notes
## [1] "Variable flag is constructed by CDC anthro program, values of 7 with all measures flagged were dropped from data set"
## [2] "-5 indicates individual not identified in 1998"
## [3] "Not all new cores chosen correctly but leave as is since likely to be key decision makers"
## [4] "Person codes for new and non-members (pcode >= 40) DO NOT represent the same person across split HHs"
## [5] "There are 38 split households, 3 of which are triples"
## [6] "February date indicate survey begun during pre-testing"
## [7] "Anthropometrist code incomplete, variable dropped"
## [8] "Some women 50 and over responded"
## [9] "Pregnancy history not always consistent with roster information"
## [10] "Instructions were altered so that adult parents were also measured"
## [11] "Children less than 6 months old not included"
## [12] "Calculations assume housing subsidy paid in kind"
## [13] "Must use number of facilities serving community and distance together"
## [14] "Despite instructions, a common mistake in this section is that when no facilities serving the community were identified, distance to nearest was not recorded (-4)"
## [15] "Old dates probably refer to when school first in area"
## [16] "No info for cluster 196 assumed same as neighboring 197"
## [17] "Clusters 80, 217 & 218 have little/no information on secondary schools"
## [18] "Cluster 210 missing from questionnaire"
## [19] "18"
##
## $names
## [1] "hhid93" "pcode" "idcommunity" "year" "hightreat"
## [6] "post" "postXhigh" "waz" "whz" "haz"
## [11] "fedu" "medu" "hhsizep" "lntotminc" "immuniz"
## [16] "nonclinic" "male" "age" "age93_0" "age93_1"
## [21] "age93_2" "age93_3" "age98_0" "age98_1" "age98_2"
## [26] "age98_3"
On retrouve énormément d’élément allant du type de la nature de l’objet aux noms des différentes variables qui le compose en passant par l’indexation des observations. Toutes ces informations ne sont pas nécessairement intéressantes. On peut les appeler une à une en les ciblant à l’aide de la fonction attr() en précisant l’élément via l’option which. Isolons le label de la base.
dat %>% attr(which='label')
## [1] "Data for A. Colin Cameron (2022), ANALYSIS OF ECONOMIC DATA, Amazon"
Ces commandes peuvent être utilisées sur chaque variable. Passons en revue les attributs de la variable waz.
attributes(dat$waz)
## $label
## [1] "Weight for age z Score"
##
## $format.stata
## [1] "%6.2f"
La commande peut être utilisée pour modifier l’attribue ou même en créer un. Traduisons le label de waz et ajoutons cette traduction à la variable comme attribut trad.
attributes(dat$waz)$trad<-"z Score pondéré par l'âge"
attributes(dat$waz)
## $label
## [1] "Weight for age z Score"
##
## $format.stata
## [1] "%6.2f"
##
## $trad
## [1] "z Score pondéré par l'âge"
Je ne saurais que vous conseiller de systématiquement ajouter ce type d’attributs à vos variables de manière à documenter votre travail particulièrement lorsque celui a vocation à être partagé.
On peut à partir de la fonction att() et de la commande d’itération sapply() répliquer la fonction describe de Stata.
data.frame(Type_stockage=sapply(dat,typeof),
Format=sapply(dat,attr,which='format.stata'),
Label=sapply(dat,attr,which='label'))
## Type_stockage Format
## hhid93 double %10.0g
## pcode double %8.0g
## idcommunity double %10.0g
## year double %9.0g
## hightreat double %9.0g
## post double %9.0g
## postXhigh double %9.0g
## waz double %6.2f
## whz double %6.2f
## haz double %6.2f
## fedu double %9.0g
## medu double %9.0g
## hhsizep double %10.0g
## lntotminc double %9.0g
## immuniz double %8.0g
## nonclinic double %9.0g
## male double %9.0g
## age double %10.0g
## age93_0 double %9.0g
## age93_1 double %9.0g
## age93_2 double %9.0g
## age93_3 double %9.0g
## age98_0 double %9.0g
## age98_1 double %9.0g
## age98_2 double %9.0g
## age98_3 double %9.0g
## Label
## hhid93 Household identification No
## pcode 93: Person Code
## idcommunity Identifier for community
## year = 93 or 98
## hightreat = 1 if community has clinic in 1993
## post = 1 if year==98 and =0 if year==93
## postXhigh = post times hightreat
## waz Weight for age z Score
## whz Weight for height z Score
## haz Height for age z Score
## fedu Father's education
## medu Mother's education
## hhsizep Household size (all members)
## lntotminc Log of monthly household income
## immuniz Number of immunization campaigns since 1993
## nonclinic Number of health facilities other than clinic in 1993
## male = 1 if male
## age Precise age in fractions of year
## age93_0 =1 if age 0 in 1993
## age93_1 =1 if age 1 in 1993
## age93_2 =1 if age 2 in 1993
## age93_3 =1 if age 3 in 1993
## age98_0 =1 if age 0 in 1998
## age98_1 =1 if age 1 in 1998
## age98_2 =1 if age 2 in 1998
## age98_3 =1 if age 3 in 1998
Le même type de résultat moins ergonomique peut être obtenu via str().
dat %>% str()
## tibble [1,071 × 26] (S3: tbl_df/tbl/data.frame)
## $ hhid93 : num [1:1071] 590020 590100 590100 590110 590070 ...
## ..- attr(*, "label")= chr "Household identification No"
## ..- attr(*, "format.stata")= chr "%10.0g"
## $ pcode : num [1:1071] 41 41 9 40 5 44 45 43 40 9 ...
## ..- attr(*, "label")= chr "93: Person Code"
## ..- attr(*, "format.stata")= chr "%8.0g"
## $ idcommunity: num [1:1071] 59 59 59 59 59 59 59 59 59 59 ...
## ..- attr(*, "label")= chr "Identifier for community"
## ..- attr(*, "format.stata")= chr "%10.0g"
## $ year : num [1:1071] 98 98 93 98 93 98 98 98 98 93 ...
## ..- attr(*, "label")= chr "= 93 or 98"
## ..- attr(*, "format.stata")= chr "%9.0g"
## $ hightreat : num [1:1071] 1 1 1 1 1 1 1 1 1 1 ...
## ..- attr(*, "label")= chr "= 1 if community has clinic in 1993"
## ..- attr(*, "format.stata")= chr "%9.0g"
## $ post : num [1:1071] 1 1 0 1 0 1 1 1 1 0 ...
## ..- attr(*, "label")= chr "= 1 if year==98 and =0 if year==93"
## ..- attr(*, "format.stata")= chr "%9.0g"
## $ postXhigh : num [1:1071] 1 1 0 1 0 1 1 1 1 0 ...
## ..- attr(*, "label")= chr "= post times hightreat"
## ..- attr(*, "format.stata")= chr "%9.0g"
## $ waz : num [1:1071] 0.73 0.64 -0.17 0.59 -0.62 ...
## ..- attr(*, "label")= chr "Weight for age z Score"
## ..- attr(*, "format.stata")= chr "%6.2f"
## ..- attr(*, "trad")= chr "z Score pondéré par l'âge"
## $ whz : num [1:1071] 1.18 2.72 -0.44 4.62 0.74 ...
## ..- attr(*, "label")= chr "Weight for height z Score"
## ..- attr(*, "format.stata")= chr "%6.2f"
## $ haz : num [1:1071] -0.22 -2.6 -0.04 -5.68 -2.06 ...
## ..- attr(*, "label")= chr "Height for age z Score"
## ..- attr(*, "format.stata")= chr "%6.2f"
## $ fedu : num [1:1071] 0 0 0 0 0 0 0 9 10 8 ...
## ..- attr(*, "label")= chr "Father's education"
## ..- attr(*, "format.stata")= chr "%9.0g"
## $ medu : num [1:1071] 7 0 10 7 6 10 8 0 0 6 ...
## ..- attr(*, "label")= chr "Mother's education"
## ..- attr(*, "format.stata")= chr "%9.0g"
## $ hhsizep : num [1:1071] 6 7 9 6 5 16 16 16 10 9 ...
## ..- attr(*, "label")= chr "Household size (all members)"
## ..- attr(*, "format.stata")= chr "%10.0g"
## $ lntotminc : num [1:1071] 8.56 7.7 7.94 7.71 4.94 ...
## ..- attr(*, "label")= chr "Log of monthly household income"
## ..- attr(*, "format.stata")= chr "%9.0g"
## $ immuniz : num [1:1071] 2 2 0 2 0 2 2 2 2 0 ...
## ..- attr(*, "label")= chr "Number of immunization campaigns since 1993"
## ..- attr(*, "format.stata")= chr "%8.0g"
## $ nonclinic : num [1:1071] 0 0 0 0 0 0 0 0 0 0 ...
## ..- attr(*, "label")= chr "Number of health facilities other than clinic in 1993"
## ..- attr(*, "format.stata")= chr "%9.0g"
## $ male : num [1:1071] 1 0 1 1 0 0 1 0 0 0 ...
## ..- attr(*, "label")= chr "= 1 if male"
## ..- attr(*, "format.stata")= chr "%9.0g"
## $ age : num [1:1071] 3.11 3.02 0 3.09 1.5 ...
## ..- attr(*, "label")= chr "Precise age in fractions of year"
## ..- attr(*, "format.stata")= chr "%10.0g"
## $ age93_0 : num [1:1071] 0 0 1 0 0 0 0 0 0 0 ...
## ..- attr(*, "label")= chr "=1 if age 0 in 1993"
## ..- attr(*, "format.stata")= chr "%9.0g"
## $ age93_1 : num [1:1071] 0 0 0 0 1 0 0 0 0 0 ...
## ..- attr(*, "label")= chr "=1 if age 1 in 1993"
## ..- attr(*, "format.stata")= chr "%9.0g"
## $ age93_2 : num [1:1071] 0 0 0 0 0 0 0 0 0 0 ...
## ..- attr(*, "label")= chr "=1 if age 2 in 1993"
## ..- attr(*, "format.stata")= chr "%9.0g"
## $ age93_3 : num [1:1071] 0 0 0 0 0 0 0 0 0 1 ...
## ..- attr(*, "label")= chr "=1 if age 3 in 1993"
## ..- attr(*, "format.stata")= chr "%9.0g"
## $ age98_0 : num [1:1071] 0 0 0 0 0 0 0 0 0 0 ...
## ..- attr(*, "label")= chr "=1 if age 0 in 1998"
## ..- attr(*, "format.stata")= chr "%9.0g"
## $ age98_1 : num [1:1071] 0 0 0 0 0 1 1 0 0 0 ...
## ..- attr(*, "label")= chr "=1 if age 1 in 1998"
## ..- attr(*, "format.stata")= chr "%9.0g"
## $ age98_2 : num [1:1071] 0 0 0 0 0 0 0 0 0 0 ...
## ..- attr(*, "label")= chr "=1 if age 2 in 1998"
## ..- attr(*, "format.stata")= chr "%9.0g"
## $ age98_3 : num [1:1071] 1 1 0 1 0 0 0 1 1 0 ...
## ..- attr(*, "label")= chr "=1 if age 3 in 1998"
## ..- attr(*, "format.stata")= chr "%9.0g"
## - attr(*, "label")= chr "Data for A. Colin Cameron (2022), ANALYSIS OF ECONOMIC DATA, Amazon"
## - attr(*, "notes")= chr [1:19] "Variable flag is constructed by CDC anthro program, values of 7 with all measures flagged were dropped from data set" "-5 indicates individual not identified in 1998" "Not all new cores chosen correctly but leave as is since likely to be key decision makers" "Person codes for new and non-members (pcode >= 40) DO NOT represent the same person across split HHs" ...
Le package labelled est spécifiquement pour travailler les bases dans lesquelles les variables présentes des attributs. Sa fonction look_for() permet d’obtenir un tableau proche de celui présenté par describe dans Stata. Je ne saurai que recommander son utilisation.
dat %>% look_for()
## pos variable label col_type missing values
## 1 hhid93 Household identification No dbl 0
## 2 pcode 93: Person Code dbl 0
## 3 idcommunity Identifier for community dbl 0
## 4 year = 93 or 98 dbl 0
## 5 hightreat = 1 if community has clinic in 1993 dbl 0
## 6 post = 1 if year==98 and =0 if year==93 dbl 0
## 7 postXhigh = post times hightreat dbl 0
## 8 waz Weight for age z Score dbl 0
## 9 whz Weight for height z Score dbl 0
## 10 haz Height for age z Score dbl 0
## 11 fedu Father's education dbl 0
## 12 medu Mother's education dbl 0
## 13 hhsizep Household size (all members) dbl 0
## 14 lntotminc Log of monthly household income dbl 0
## 15 immuniz Number of immunization campaigns sinc~ dbl 0
## 16 nonclinic Number of health facilities other tha~ dbl 0
## 17 male = 1 if male dbl 0
## 18 age Precise age in fractions of year dbl 0
## 19 age93_0 =1 if age 0 in 1993 dbl 0
## 20 age93_1 =1 if age 1 in 1993 dbl 0
## 21 age93_2 =1 if age 2 in 1993 dbl 0
## 22 age93_3 =1 if age 3 in 1993 dbl 0
## 23 age98_0 =1 if age 0 in 1998 dbl 0
## 24 age98_1 =1 if age 1 in 1998 dbl 0
## 25 age98_2 =1 if age 2 in 1998 dbl 0
## 26 age98_3 =1 if age 3 in 1998 dbl 0
Ceci étant posé. Continuons notre exploration de la base. Les différents relevés sont réalisés sur des zones administratives qui sont ou non équipées en clinique. Si elles le sont, elle permet d’identifier le groupe de traités. Si elles ne le sont pas, elle permet d’identifier le groupe de contrôle.
data.frame(nb_communauté=length(unique(dat$idcommunity)),
com_traitée=length(unique(dat$idcommunity[which(dat$hightreat==1)])),
com_controle=length(unique(dat$idcommunity[which(dat$hightreat==0)])))
## nb_communauté com_traitée com_controle
## 1 54 26 28
Les mesures sont faites sur 54 communautés dont 26 disposent de cliniques et 28 n’en disposent pas. Voyons ce qu’il en est des observations pour chaque communauté. Ce faisant, identifions les observations pré et post traitement (le passage à la gratuité en 94).
dat %>% group_by(idcommunity) %>%
summarise(nb_obs=n(),
statut=ifelse(sum(hightreat)==nb_obs,"traité","control"),
obs_pre=nb_obs-sum(post),
obs_post=sum(post))
## # A tibble: 54 × 5
## idcommunity nb_obs statut obs_pre obs_post
## <dbl> <int> <chr> <dbl> <dbl>
## 1 59 12 traité 5 7
## 2 70 15 control 7 8
## 3 71 16 traité 10 6
## 4 74 3 control 2 1
## 5 77 1 traité 1 0
## 6 78 23 traité 14 9
## 7 79 20 control 10 10
## 8 80 32 traité 8 24
## 9 193 19 traité 13 6
## 10 195 10 traité 6 4
## # ℹ 44 more rows
On remarque que le nombre d’observations est différent d’une communauté à l’autre et que ce nombre différent également entre la période pré et la période post traitement. Nous n’avons donc pas ici un panel cylindré autrement-dit des individus suivis sur l’ensemble de la durée de l’étude. Cela n’est pas dû à une disparition des individus sur la période post traitement dans la mesure où pour certaine communauté le nombre d’observations est supérieure après le traitement.
Voyons ce qu’il en est globalement. Comptons le nombre d’observations du groupe des traités et du groupe de contrôle, toujours en distinguant les périodes avant et après traitement.
dat %>%
mutate(traite=ifelse(dat$hightreat==1,'traité','controle')) %>%
select(traite,post) %>%
group_by(traite) %>%
summarise(n=n(),av=n-sum(post),ap=sum(post),
ev=ap-av)
## # A tibble: 2 × 5
## traite n av ap ev
## <chr> <int> <dbl> <dbl> <dbl>
## 1 controle 613 325 288 -37
## 2 traité 458 246 212 -34
Il y a quelque soit le groupe traités ou contrôles une diminution du nombre d’observations avec le temps.
On peut par ailleurs noter que les mesures avant/après ne sont prises qu’à deux dates: 1993, juste avant la réforme, et 1998, 4 ans après. A cette date la différence entre l’équipement en clinique des communautés est devenue petite. L’équipement est alors bien plus homogène.
dat %>%
mutate(traite=ifelse(dat$hightreat==1,'traité','controle')) %>%
select(traite,year) %>%
summarise(n=n(),.by=c(traite,year))
## # A tibble: 4 × 3
## traite year n
## <chr> <dbl> <int>
## 1 traité 98 212
## 2 traité 93 246
## 3 controle 93 325
## 4 controle 98 288
Pour avoir une vision encore plus claire de là structure de données, prenons le cas d’une communauté en particulier et examinons-la en détail. Prenons la numéro 59.
dat %>% filter(idcommunity==59)
## # A tibble: 12 × 26
## hhid93 pcode idcommunity year hightreat post postXhigh waz whz
## <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 590020 41 59 98 1 1 1 0.73 1.18
## 2 590100 41 59 98 1 1 1 0.64 2.72
## 3 590100 9 59 93 1 0 0 -0.170 -0.440
## 4 590110 40 59 98 1 1 1 0.59 4.62
## 5 590070 5 59 93 1 0 0 -0.620 0.740
## 6 590090 44 59 98 1 1 1 3.48 6.02
## 7 590090 45 59 98 1 1 1 1.67 4.59
## 8 590090 43 59 98 1 1 1 1.08 0.93
## 9 590040 40 59 98 1 1 1 0.3 1.37
## 10 590010 9 59 93 1 0 0 2.5 2.13
## 11 590040 9 59 93 1 0 0 -0.460 0.130
## 12 590070 4 59 93 1 0 0 -1.54 -0.0600
## # ℹ 17 more variables: haz <dbl>, fedu <dbl>, medu <dbl>, hhsizep <dbl>,
## # lntotminc <dbl>, immuniz <dbl>, nonclinic <dbl>, male <dbl>, age <dbl>,
## # age93_0 <dbl>, age93_1 <dbl>, age93_2 <dbl>, age93_3 <dbl>, age98_0 <dbl>,
## # age98_1 <dbl>, age98_2 <dbl>, age98_3 <dbl>
Plusieurs ménages ont plus d’une entrée dans l’extrait. Le ménage 590100 a deux entrées, le ménage 590090 en a trois. Jetons un oeil au premier.
dat %>% filter(idcommunity==59 & hhid93==590100)
## # A tibble: 2 × 26
## hhid93 pcode idcommunity year hightreat post postXhigh waz whz haz
## <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 590100 41 59 98 1 1 1 0.64 2.72 -2.6
## 2 590100 9 59 93 1 0 0 -0.170 -0.440 -0.0400
## # ℹ 16 more variables: fedu <dbl>, medu <dbl>, hhsizep <dbl>, lntotminc <dbl>,
## # immuniz <dbl>, nonclinic <dbl>, male <dbl>, age <dbl>, age93_0 <dbl>,
## # age93_1 <dbl>, age93_2 <dbl>, age93_3 <dbl>, age98_0 <dbl>, age98_1 <dbl>,
## # age98_2 <dbl>, age98_3 <dbl>
Les deux mesures sont réalisées sur l’échantillon des traités (hightreat égale à 1: là où il y a des cliniques en 1993). Il y en a une réalisée pré traitement et une post traitement. La mesure de 93 est faite sur une fille de moins d’un 1 et la seconde en 98 sur un garçon de 3 ans.
On est loin d’avoir un panel la spécification en two ways fixed effect (TWFE) n’est donc pas envisageable. nous resterons donc sur la spécification classique.
Maintenant que nous avons une visions plus claire de la structure de la base. Passons à l’analyse des variables. Revenons pour cela sur la définition de la variable expliquée waz. Il s’agit d’un Z-score autrement-dit le nombre d’écart-types qui sépare la valeur observée de la moyenne sur le groupe étudié. Ici, la mesure porte sur le poids des enfants. Le groupe étudié est défini par l’age de l’enfant. Ainsi, par exemple, une valeur de waz de -1 correspond à un enfant qui présenterait un poids inférieur de 1 écart-type à la moyenne des enfants de son âge. Plus cette mesure est négative, plus on est face à des enfants en mauvaise santé. Si le traitement, la gratuité des soins, est efficace après sa mise en oeuvre, on devrait constater une augmentation de la variable plus importante que celle constatée sur groupe de contrôle. Autrement-dit, on attend une Diff in Diff positive.
Pour commencer, limitons notre analyse à une Diff in Diff simple sans variables de contrôles. Ne gardons que waz, hightreat, post, postXhigh et idcommunity.
dat_red_1<- dat %>% select(waz,hightreat,post,postXhigh,idcommunity)
Présentons quelques statistiques descriptives sur le modèle ce que propose Stata par défaut avec sa fonction summarize.
dat_red_1 %>%
select(-idcommunity) %>%
summarize_all(
.fun=list(~length(.),~mean(.),~sd(.),~min(.),~max(.))) %>%
unlist %>% matrix(nrow = 4, ncol=5) %>% as.data.frame() %>%
cbind(Variables=names(dat_red_1)[1:4],.) %>%
rename(Obs=V1,Mean=V2,Std.dev.=V3,Min=V4,Max=V5)
## Variables Obs Mean Std.dev. Min Max
## 1 waz 1071 -0.2058730 1.5874317 -5.88 4.94
## 2 hightreat 1071 0.4276377 0.4949671 0.00 1.00
## 3 post 1071 0.4668534 0.4991332 0.00 1.00
## 4 postXhigh 1071 0.1979458 0.3986373 0.00 1.00
waz est la seule variable continue du lot. On peut noter qu’elle est en moyenne négative. Les enfants objets de l’étude sont les enfants de famille pauvres. Ils présentent pour leur age un poids inférieur à la moyenne (des enfants du même age dans la population générale). Les autres variables sont catégorielles. Leur moyenne correspond à des proportions. On a ainsi 42,7% des observations qui sont faîtes sur le groupe traité, 46,6% qui sont réalisées après 1994 et 19,7% des observations qui sont faîtes sur les traités après 94.
Analyses 2x2 Diff in Diff
Passons donc à l’analyse. Commençons par établir la matrice 2x2 des moyennes (contrôle/traité, avant/après).
moy_av_t<-dat_red_1 %>% filter(post==0&hightreat==0) %>%
summarise(av_t=mean(waz))
moy_ap_t<-dat_red_1 %>% filter(post==1&hightreat==0) %>%
summarise(ap_t=mean(waz))
moy_av_nt<-dat_red_1 %>% filter(post==0&hightreat==1) %>%
summarise(av_nt=mean(waz))
moy_ap_nt<-dat_red_1 %>% filter(post==1&hightreat==1) %>%
summarise(ap_nt=mean(waz))
mat<-c(moy_av_t,moy_ap_t,moy_av_nt,moy_ap_nt) %>% unlist() %>%
matrix(ncol=2,nrow=2,byrow=FALSE)
rownames(mat)<-c("avant","apres")
colnames(mat)<-c("control","traite")
rm(moy_av_t,moy_ap_t,moy_av_nt,moy_ap_nt)
mat
## control traite
## avant -0.41418462 -0.5452439
## apres -0.06909722 0.3214623
A partir de là, il est facile de calculer notre Diff in Diff. Il suffit de soustraire à l’évolution avant/après mesurée sur les traités l’évolution avant/après mesurée sur le groupe de contrôle.
(mat[2,2]-mat[1,2])-(mat[2,1]-mat[1,1])
## [1] 0.5216188
rm(mat)
On a une augmentation de 0,52 points supérieurs de waz pour le groupe des traités (ceux qui avez une cliniques dans la communauté en 1993) par rapport au groupe de contrôle.
Le même résultat peut être obtenu à partir de la syntaxe dplyr.
moy<-dat_red_1 %>%
group_by(hightreat,post) %>%
summarise(moyenne=mean(waz),.groups='keep')
moy
## # A tibble: 4 × 3
## # Groups: hightreat, post [4]
## hightreat post moyenne
## <dbl> <dbl> <dbl>
## 1 0 0 -0.414
## 2 0 1 -0.0691
## 3 1 0 -0.545
## 4 1 1 0.321
moy<-moy%>%
pull(moyenne)
(moy[4]-moy[2])-(moy[3]-moy[1])
## [1] 0.5216188
Le passage à la gratuité semble bien permettre d’améliorer la santé des enfants pauvres pour qui les barrières non tarifaires ne sont pas un problème (ceux ayant accès à une clinique). Mais pour conclure, il nous faut un test statistique sur cette Diff in Diff. Pour l’obtenir, procédons à la régression habituelle. Concernant les tests sur les coefficients, nous optons pour un clustering sur la communauté. On postule donc qu’il y a plus de variance entre communautés qu’à l’intérieure des communautés.
reg<-lm(waz~hightreat+post+postXhigh,data=dat_red_1)
coeftest(reg,vcov. = vcovCL(reg,cluster=dat_red_1$idcommunity))
##
## t test of coefficients:
##
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) -0.41418 0.11514 -3.5972 0.0003365 ***
## hightreat -0.13106 0.19681 -0.6659 0.5056042
## post 0.34509 0.13710 2.5170 0.0119810 *
## postXhigh 0.52162 0.23530 2.2168 0.0268447 *
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Le test confirme bien le résultat.
On peut reconstituer la matrice des moyennes à partir des différents coefficients de la régression DID.
La constante correspond au waz moyen sur le groupe de contrôle avant la date du traitement (traite==0 , post==0 et donc traite::post==0). Pour obtenir, la moyenne sur le groupe de contrôle après la date du traitement (traite==0 , post==1 et donc traite::post==0), on additionne simplement la constante et le coefficient associé à post. Pour le groupe des traités avant le traitement (traite==1, post==0 et donc traite::post==0), on additionne simplement la constante et le coefficient associé à traite. Pour les traités post traitement (traite==1, post==1 et donc traite::post==1), il suffit de tout additionner constante compris.
cont_av<-coef(reg)[1] %>% sum()
cont_ap<-coef(reg)[c(1,3)] %>% sum()
trait_av<-coef(reg)[c(1,2)] %>% sum()
trait_ap<-coef(reg) %>% sum()
mat<-matrix(c(cont_av,cont_ap,trait_av,trait_ap),ncol=2)
colnames(mat)<-c('Controle','Traite')
rownames(mat)<-c('avant','aprés')
mat
## Controle Traite
## avant -0.41418462 -0.5452439
## aprés -0.06909722 0.3214623
La Diff in Diff évaluée est en quelque sorte brute (simple) dans la mesure où il n’y a pas de variables de contrôle. On peut sans difficulté en ajouter (mais cela peut poser un problème de bad controls dans certaines circonstances dont nous discuterons dans un prochain post). Faisons-le introduisons un effet fixe communauté, l’éducation de parent (homme et femme), la taille du ménage, le revue, le nombre de campagne de vaccinations sur la période, le nombre de clinique dans la communauté et si l’enfant est un garçon ou une fille. Sortons uniquement le coefficient donnant la Diff in Diff.
reg_<-lm(waz~hightreat+post+postXhigh+
factor(idcommunity)+fedu+medu+hhsizep+lntotminc+immuniz+nonclinic,
data=dat)
coeftest(reg_)[4,]
## Estimate Std. Error t value Pr(>|t|)
## 0.642880667 0.212103613 3.030974622 0.002499841
Ici, il n’est pas possible de reconstituer la matrice des moyennes dans la mesure où la constante est ajustée en fonction de l’ensemble des variables catégorielles de la régression (notamment ici les effets fixes communauté et le sexe de l’enfant…).
Ici, il est impossible de procéder à un quelconque test de parallel trend ou même un test placebo faute de granularité dans les données. Nous terminerons donc ce premier exemple avec un graphe. Pour cela, préparons les données mobilisées.
bas_g<-data.frame(traite=c("Controle","Controle","Traite","Traite"),
post=c("1993","1998","1993","1998"),
waz=c(mat[1,1],mat[2,1],
mat[1,2],mat[2,2]))
did<-coef(reg_)[4]
bas_g
## traite post waz
## 1 Controle 1993 -0.41418462
## 2 Controle 1998 -0.06909722
## 3 Traite 1993 -0.54524390
## 4 Traite 1998 0.32146226
Puis, établissons notre graphe.
ggplot(bas_g, aes(x = post,
y = waz,
color = traite)) +
geom_point() +
geom_line(aes(group = traite)) +
labs(title="Effet du passage à la gratuité sur la santé des enfants pauvres",
y="Z-score du poids moyen pondéré par classes d'âge",
color="")+
annotate(geom = "segment", x = "1993",
xend = "1998",
y = bas_g$waz[3],
yend = bas_g$waz[4] - did,
linetype = "dashed", color = "grey50") +
annotate(geom = "segment", x = "1998",
xend = "1998",
y = bas_g$waz[4] ,
yend = bas_g$waz[4] - did,
linetype = "dotted", color = "blue") +
annotate(geom = "label", x = "1998",
y = bas_g$waz[4] - (did / 2),
label = "effet du programme", size = 3)+
theme_bw()+
theme(
plot.title = element_text(hjust=0.5),
axis.title.x = element_blank(),
legend.position = "top"
)
rm(list = ls(all.names = TRUE))
Difference in Difference avec périodes multiples
Pour le second exemple développé, nous changeons de configuration avec cette fois plus de deux périodes de mesures (avant et après le traitement) et pour cela nous utiliserons une des bases de données mobilisées dans le manuel de Stata pour illustrer l’usage de sa fonction didregress. Dans cet application, on a un fournisseur de soins de santé qui gère des hôpitaux qui s’interroge sur l’impact de l’automatisation des procédures d’admission sur la satisfaction des patients. On a des données mensuelles sur les patients entre janvier et juillet. La procédure a été mise en place en avril dans une partie de ces hôpitaux.
Chargement et exploration des données
Les données sont à charger à partir de l’url associé au manuel de Stata 17. Pour cela, il suffit d’introduire l’adresse comme argument dans la fonction read_dta().
dat2<-read_dta("https://www.stata-press.com/data/r17/hospdd.dta")
Ceci étant fait, voyons ce que contient la base. Comptons le nombre d’observation et le nombre de variables.
c(nb_obs=nrow(dat2),nb_var=length(dat2))
## nb_obs nb_var
## 7368 5
On a 7 368 observations et 5 variables. Mais que représentent ces variables? Utilisons notre réplication de describe pour accéder aux labels des variables.
data.frame(Type_stockage=sapply(dat2,typeof),
Format=sapply(dat2,attr,which='format.stata'),
Label=sapply(dat2,attr,which='label'))
## Type_stockage Format Label
## hospital double %9.0g Hospital ID
## frequency double %9.0g Hospital visit frequency
## month double %8.0g Month
## procedure double %9.0g Admission procedure
## satis double %9.0g Patient satisfaction score
Les commentaires laissés ne sont pas très parlant. Revenons aux commandes R classique avec str().
str(dat2)
## tibble [7,368 × 5] (S3: tbl_df/tbl/data.frame)
## $ hospital : num [1:7368] 1 1 1 1 1 1 1 1 1 1 ...
## ..- attr(*, "label")= chr "Hospital ID"
## ..- attr(*, "format.stata")= chr "%9.0g"
## $ frequency: dbl+lbl [1:7368] 3, 2, 4, 2, 1, 1, 2, 4, 2, 2, 4, 1, 2, 3, 4, 2, 1, 2,...
## ..@ label : chr "Hospital visit frequency"
## ..@ format.stata: chr "%9.0g"
## ..@ labels : Named num [1:4] 1 2 3 4
## .. ..- attr(*, "names")= chr [1:4] "Low" "Medium" "High" "Very high"
## $ month : dbl+lbl [1:7368] 7, 3, 2, 4, 3, 7, 4, 1, 3, 1, 1, 4, 6, 1, 1, 4, 1, 2,...
## ..@ label : chr "Month"
## ..@ format.stata: chr "%8.0g"
## ..@ labels : Named num [1:7] 1 2 3 4 5 6 7
## .. ..- attr(*, "names")= chr [1:7] "January" "February" "March" "April" ...
## $ procedure: dbl+lbl [1:7368] 1, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0,...
## ..@ label : chr "Admission procedure"
## ..@ format.stata: chr "%9.0g"
## ..@ labels : Named num [1:2] 0 1
## .. ..- attr(*, "names")= chr [1:2] "Old" "New"
## $ satis : num [1:7368] 4.11 3.32 3.41 3 3.11 ...
## ..- attr(*, "label")= chr "Patient satisfaction score"
## ..- attr(*, "format.stata")= chr "%9.0g"
## - attr(*, "label")= chr "Artificial hospital admission procedure data"
Les choses sont plus claires. hospital est l’identifiant des hôptiaux. frequency un indicateur de la fréquentation des hôpitaux par le patient avec 5 valeurs (1 Low … 5 Very hight). month est le mois de la visite. Il va de 1 pour janvier à 7 pour juillet. procedure, qui est notre variable d’intérêt, prend la valeur 1 à la fois s’il l’hôpital est traité (il a automatisé sa procédure d’admission à partir d’avril) et si l’observation est effectuée en avril en après (après la mise en place du traité). satis est notre variable expliquée. Il s’agit d’une moyenne de différentes notes de 0 (pas satisfait du tout à 10 très très satisfait) attribuées par les patients sur quatre critères concernant l’admission.
look_for() permet d’obtenir quelque chose d’un peu mieux.
dat2 %>% look_for()
## pos variable label col_type missing values
## 1 hospital Hospital ID dbl 0
## 2 frequency Hospital visit frequency dbl+lbl 0 [1] Low
## [2] Medium
## [3] High
## [4] Very high
## 3 month Month dbl+lbl 0 [1] January
## [2] February
## [3] March
## [4] April
## [5] May
## [6] June
## [7] July
## 4 procedure Admission procedure dbl+lbl 0 [0] Old
## [1] New
## 5 satis Patient satisfaction score dbl 0
Voyons ce que donne ces variables. Regardons les premières observations.
dat2 %>% head()
## # A tibble: 6 × 5
## hospital frequency month procedure satis
## <dbl> <dbl+lbl> <dbl+lbl> <dbl+lbl> <dbl>
## 1 1 3 [High] 7 [July] 1 [New] 4.11
## 2 1 2 [Medium] 3 [March] 0 [Old] 3.32
## 3 1 4 [Very high] 2 [February] 0 [Old] 3.41
## 4 1 2 [Medium] 4 [April] 1 [New] 3.00
## 5 1 1 [Low] 3 [March] 0 [Old] 3.11
## 6 1 1 [Low] 7 [July] 1 [New] 2.88
On peut noter que l’on a pour un même hôpital et un même mois plusieurs observations. Simplement il y a plusieurs patients admis par mois. Rein d’étonnant. Mais examinons cela de plus prés. Combien y a t-il d’hôpital ?
length(unique(dat2$hospital))
## [1] 46
On a 46 hôpitaux. Combien y a-t-il d’observations par hôpital?
dat2 %>% count(hospital) %>% summarise(moy=mean(n))
## # A tibble: 1 × 1
## moy
## <dbl>
## 1 160.
On a en moyenne 160 observations par hôpital. Combien y a t-il d’observations par mois pour chaque hôpital?
dat2 %>% group_by(hospital,month) %>% count()
## # A tibble: 322 × 3
## # Groups: hospital, month [322]
## hospital month n
## <dbl> <dbl+lbl> <int>
## 1 1 1 [January] 46
## 2 1 2 [February] 23
## 3 1 3 [March] 23
## 4 1 4 [April] 23
## 5 1 5 [May] 23
## 6 1 6 [June] 23
## 7 1 7 [July] 23
## 8 2 1 [January] 42
## 9 2 2 [February] 21
## 10 2 3 [March] 21
## # ℹ 312 more rows
On peut noter qu’il y a toujours plus d’observations sur le mois de janvier (1) que sur les autres et cela sur tout les hôpitaux.
Ok, disons que l’on a une vision générale de la structure des données. Intéressons nous à la répartition du traitement. Voyons combien on a d’hôpitaux traités et combien constituent le groupe de contrôle.
cont<-dat2 %>%
group_by(hospital) %>%
summarise(b=ifelse(sum(procedure)==0,"Controles","Traités"))
table(cont$b)
##
## Controles Traités
## 28 18
rm(cont)
Ce dénombrement apparaît dans la première partie de la sortie proposée par la fonction didregress de Stata. La seconde partie donne une information sur la première fois où on observe les groupes dans la base (comme non traité et comme traité). Répliquons-la.
ent<-dat2 %>%
group_by(hospital) %>%
mutate(b=ifelse(sum(procedure)==0,"Controles","Traités"),
m=month) %>%
group_by(hospital,b,procedure) %>%
summarise(min_=min(month), .groups = 'keep')
cbind(Controle=
c(minimum=min(ent$min_[which(ent$procedure==0&ent$b=="Controles")]),
maximum=max(ent$min_[which(ent$procedure==0&ent$b=="Controles")])),
Traite=
c(minimum=min(ent$min_[which(ent$procedure==1&ent$b=="Traités")]),
maximum=max(ent$min_[which(ent$procedure==1&ent$b=="Traités")])))
## Controle Traite
## minimum 1 4
## maximum 1 4
Ici, on a une base homogène. Toutes les observations du groupe de contrôle entre dans la base dés le mois de janvier et le traitement est appliqué pour tous les traités en avril. On est sur une Diff in Diff classique pas une staggered.
Avant de la mettre en oeuvre, jetons un œil sur la distribution de la variable expliquée. Utilisons pour cela un histogramme rapidement fait.
ggplot(data=dat2,aes(x=satis))+
geom_histogram(color='black',bins=30)+
geom_vline(xintercept = mean(dat2$satis),color='red')+
theme_minimal()
Elle présente une forme très proche de celle d’une loi normale.
Analyse Diff in Diff multipériodes
Maintenant que nous connaissons mieux la structure de nos données, on a des cross-sections répétées (pas un panel), nous pouvons procéder à l’estimation de notre Diff in Diff. Pour cela, nous utiliserons les TWFE (two ways fixed effects). Pour les tests sur le coefficient d’intérêt, dans la mesure où le recueil des données se fait par hôpitaux (et donc qu’il doit y avoir plus de variabilité entre les différents hôpitaux que sur les observations par hôpitaux), on utilise des erreurs standards clusterisées sur la base des hôpitaux\[ satis_{i,t,s}=\gamma_i+\gamma_t+D_{s,t}+\epsilon_{i,t,s} \]
où i correspond à un hôpital, t à un mois et s un individu. D indique si l’individu a été traité à la date t (ici s’il est passé par un hôpital qui a la nouvelle procédure le mois où il a été soigné…).
On a ainsi:
reg_2<-lm(satis~procedure+factor(hospital)+factor(month),
data=dat2)
coeftest(reg_2,vcov.=vcovCL(reg_2,cluster=dat2$hospital))[2,] %>%
round(digits = 7)
## Estimate Std. Error t value Pr(>|t|)
## 0.8479879 0.0321121 26.4071077 0.0000000
Le test confirme l’effet positif de l’automatisation de la procédure d’admission. Cette automatisation permet en moyenne d’augmenter la note de satisfaction moyenne de 0,85 points. Autrement-dit, si les hôpitaux traités (automatisés) ne l’avez pas été, la satisfaction aurait été 0,85 points plus bas.
Le même résultat peut être obtenu à partir d’une spécification classique en 2x2 Diff in Diff. Pour cela, il est nécessaire de commencer par réaliser quelque manipulations de données. Générons les variables Traite et post.
dat2_<-dat2 %>%
group_by(hospital) %>%
mutate(Traite=ifelse(sum(procedure)==0,"Controles","Traités"),
post=month>=4)
Maintenant que l’on a les données pro format, procédons à la régression et sortons uniquement la ligne associée au coefficient Diff in Diff.
reg_<-lm(satis~Traite+post+Traite*post,data=dat2_)
coeftest(reg_,vcov.=vcovCL(reg_,cluster=dat2$hospital))[4,] %>%
round(digits = 7)
## Estimate Std. Error t value Pr(>|t|)
## 0.8479879 0.0320051 26.4954049 0.0000000
La spécification en TWFE ne permet pas de reconstituer la matrice des moyennes contrairement à celle en 2x2 Diff in Diff que nous venons de mettre en oeuvre. Établissons-la à partir des coefficients.
cont_av<-coef(reg_)[1] %>% sum()
cont_ap<-coef(reg_)[c(1,3)] %>% sum()
trait_av<-coef(reg_)[c(1,2)] %>% sum()
trait_ap<-coef(reg_) %>% sum()
mat<-matrix(c(cont_av,cont_ap,trait_av,trait_ap),ncol=2)
colnames(mat)<-c('Controle','Traite')
rownames(mat)<-c('avant','aprés')
rm(cont_av,cont_ap,trait_av,trait_ap)
mat
## Controle Traite
## avant 3.392509 3.525383
## aprés 3.382490 4.363351
Vérifions que l’ensemble est juste en recalculons notre double différence.
(mat[2,2]-mat[1,2])-(mat[2,1]-mat[1,1])
## [1] 0.8479879
Présentons notre résultat sur la forme d’un graphe. Pour cela, passons notre matrice de moyen en data frame et donnons lui une forme adéquate.
bas_g<-data.frame(traite=c("Controle","Controle","Traite","Traite"),
post=c("avant avril","avril et aprés avril",
"avant avril","avril et aprés avril"),
satis=c(mat[1,1],mat[2,1],
mat[1,2],mat[2,2]))
did<-coef(reg_)[4]
rm(mat)
bas_g
## traite post satis
## 1 Controle avant avril 3.392509
## 2 Controle avril et aprés avril 3.382490
## 3 Traite avant avril 3.525383
## 4 Traite avril et aprés avril 4.363351
Maintenant que nous avons notre data frame, réalisons le graphe.
ggplot(bas_g, aes(x = post,
y = satis,
color = traite)) +
geom_point() +
geom_line(aes(group = traite)) +
labs(title="Effet de la mise en place de l'automatisation des admissions",
y="Note moyenne de satisfaction",
color="")+
annotate(geom = "segment", x = "avant avril",
xend = "avril et aprés avril",
y = bas_g$satis[3],
yend = bas_g$satis[4] - did,
linetype = "dashed", color = "grey50") +
annotate(geom = "segment", x = "avril et aprés avril",
xend = "avril et aprés avril",
y = bas_g$satis[4] ,
yend = bas_g$satis[4] - did,
linetype = "dotted", color = "blue") +
annotate(geom = "label", x = "avril et aprés avril",
y = bas_g$satis[4] - (did / 2),
label = "effet du programme", size = 3)+
theme_bw()+
theme(
plot.title = element_text(hjust=0.5),
axis.title.x = element_blank(),
legend.position = "top"
)
rm(bas_g,did)
Tests complémentaires
Analyses graphiques de la parallel trend hypothesis
Examinons l’hypothèse de parallel trend. Commençons par une rapide analyse graphique. Reprenons celle développée dans le post précédant. Calculons donc les moyennes de l’outcome sur les différents mois respectivement sur le groupe des traités et des non traités (le groupe de contrôle). Créons une fonction pour faciliter les réplications qui suivront. Celle-ci doit permettre de générer un tableau présentant pour chaque mois la satisfaction moyenne sur les groupes des traités et de contrôle, ainsi que le graphe associée.
parallel_plot<-function(var1,trait,time,data,table=TRUE){
par_1<-data %>%
group_by({{trait}},{{time}}) %>%
summarise(moy_=mean({{var1}}),.groups = "keep") %>%
cbind(trait=c(rep(FALSE,7),rep(TRUE,7)))
colnames(par_1)<- c("status","month","moy_","trait")
if(table==TRUE){
par_1
}else{
ggplot(data=par_1,aes(x=month,y=moy_,
colour=trait,
shape=trait,
alpha=trait))+
geom_point()+
geom_line()+
geom_vline(xintercept = 3,
linewidth=0.1,linetype="dashed")+
geom_text(aes(label="Automatisée",
x=6,
y=4.25))+
labs(title = "Satisfaction à l'admission dans les hôpitaux",
x="mois")+
scale_x_continuous(breaks=1:7,
labels=c("Janvier","Février","Mars","Avril",
"Mai","Juin","Juillet"))+
scale_y_continuous(breaks = seq(3.2,4.4,0.2))+
scale_shape_manual(values=c(1, 16))+
scale_colour_manual(values=c("Black","Black"))+
scale_alpha_manual(values=c(0.5,1))+
theme_minimal()+
theme(
plot.title = element_text(hjust=0.5,face="bold"),
axis.title.y = element_blank(),
axis.title.x = element_blank(),
axis.line = element_line(color="black",linewidth=0.1),
axis.ticks = element_line(color="black",linewidth=0.1),
panel.grid = element_blank(),
legend.position="none")}
}
Appliquons notre nouvelle fonction pour obtenir le tableau des moyennes.
parallel_plot(satis,Traite,month,dat2_)
## # A tibble: 14 × 4
## # Groups: status, month [14]
## status month moy_ trait
## <chr> <dbl+lbl> <dbl> <lgl>
## 1 Controles 1 [January] 3.39 FALSE
## 2 Controles 2 [February] 3.38 FALSE
## 3 Controles 3 [March] 3.42 FALSE
## 4 Controles 4 [April] 3.40 FALSE
## 5 Controles 5 [May] 3.36 FALSE
## 6 Controles 6 [June] 3.39 FALSE
## 7 Controles 7 [July] 3.37 FALSE
## 8 Traités 1 [January] 3.53 TRUE
## 9 Traités 2 [February] 3.51 TRUE
## 10 Traités 3 [March] 3.53 TRUE
## 11 Traités 4 [April] 4.34 TRUE
## 12 Traités 5 [May] 4.38 TRUE
## 13 Traités 6 [June] 4.35 TRUE
## 14 Traités 7 [July] 4.38 TRUE
Générons le graphe pour visualiser plus nettement l’hypothèse de paralell trend pré traitement.
parallel_plot(satis,Traite,month,dat2_,table=FALSE)
Le graphe obtenu correspond au premier généré par la fonction estat trendplots de Stata. Il s’agit d’une commande post estimation. A sa lecture, on peut dire qu’à première vue que la parallel trend pré traitement semble tenir.
Mais allons plus loin. Essayons d’établir le second graphe. Celui-ci est obtenue en commençant par unifier le départ des deux séries puis en les prolongeant à partir d’un trend linéaire. Si la parallel trend est respecte les deux séries devrait se superposer sur le période pré traitement.
Établissons donc le point de départ en faisant la moyenne des observations sur la première période d’observation.
m1<-mean(dat2_$satis[which(dat2_$month==1)])
m1
## [1] 3.444675
Une fois celui-ci obtenu, calculons les clés d’ajustement à pratiquer sur les deux séries (Contrôles et Traités) pour leur donner la même origine.
aj_c<-m1-mean(dat2_$satis[which(dat2_$month==1&dat2_$Traite=="Controles")])
aj_t<-m1-mean(dat2_$satis[which(dat2_$month==1&dat2_$Traite=="Traités")])
c(aj_c,aj_t)
## [1] 0.05879710 -0.08259228
Appliquons-les à nos données.
dat2_1<-dat2_ %>%
mutate(satis_=ifelse(Traite=="Controles",satis+aj_c,satis+aj_t))
rm(aj_c,aj_t)
On peut vérifier que l’ajustement a été correctement effectué en calculant la moyen sur le premier mois de notre satisfaction ajustée.
mean(dat2_1$satis_[which(dat2_1$month==1)])
## [1] 3.444675
L’ajustement apparaît être correcte.
parallel_plot(satis_,Traite,month,dat2_1,table=FALSE)
Tests statistiques de la parallel trend hypothesis
L’analyse graphique peut être appuyée par un complément statistique. Le focal est toujours porté sur la période pré traitement. La parallel trend post traitement, celle qui dans l’absolu, nous intéresse n’est pas testable. Aussi, en nous reportant à la période pré traitement, nous faisant l’hypothèse qu’elle tiendrait post traitement en l’absence de traitement…
Dans les post précédant, pour tester la parallel trend hypothesis, nous avons estimé sur la période pré traitement le modèle linéaire suivant :
\[satis=month+month×traité\]
L’outcome est régressé sur une variable marquant la progression du temps et son interaction avec la binaire marquant l’appartenance au groupe des traités.
L’idée est que, si le coefficient associé à la variable d’intéraction est statistiquement différent de 0, on doit rejeter la parallel trend dans la mesure où cela montre que le trend de l’outcome sur la groupe des traités et différents de celui sur le groupe de contrôle. Appliquons la procédure à nos données.
reg_p1<-lm(satis~month+month*Traite,
data=filter(dat2_,month<4))
coeftest(reg_p1)[4,]
## Estimate Std. Error t value Pr(>|t|)
## -0.01324090 0.03888429 -0.34052052 0.73348401
rm(reg_p1)
Ici, on ne peut pas rejeté H0, autrement-dit les trend sur le groupe des traités et la groupe de contrôle semble être les mêmes ce qui va dans le sens de la parallel trend.
Stata propose un test construit sur le même principe, mais en mobilisant une spécification étendue. La spécification retenue prend pour point de départ celle de la régression en Diff-in-Diff pour lui ajoute deux variables proposant une triple interaction la première entre une variable marquant la période pré, l’appartenance au groupe de traité et la période et la seconde entre une variable marquant la période post et les mêmes variables.
On a ainsi pour une Diff-in-Diff estimé en TWFE :
\[ satis_{i,t,s}=\gamma_i+\gamma_t+D_{s,t}+\zeta_1.pre.traité.month+\zeta_2.post.traité.month+\epsilon_{i,t,s} \]
Le test porte alors sur \(\zeta_1\) si ce coefficient est statistiquement différent de zéro alors il y a une différence de trend entre les traités et le groupe de contrôle sur la période pré traitement et donc la parallel trend ne tiendrait pas. Le test pratiqué est alors un test de Wald qui établi sa statistique comme le carré du t stat ordinaire. Cette statistique suit alors une loi de Fisher à 1 et n-1 degré de liberté (n étant le nombre d’individu utilisé pour les effets fixes ici d’hôpitaux). Répliquons le test.
Commençons par préparons les données avant de procéder à l’estimation du modèle. Marquons les périodes pré et post traitement, établissons pour chaque hôpital s’il est traité ou non, enfin calculons les interactions triples.
dat2p<-dat2 %>%
mutate(pre=ifelse(month<4,1,0),
post=ifelse(month>3,1,0)) %>%
group_by(hospital) %>%
mutate(trait=ifelse(sum(procedure)==0,0,1)) %>%
ungroup() %>%
mutate(avant=pre*trait*month,
apres=post*trait*month)
Ceci fait on peut procéder à la régression, sortir le t stat du coefficient l’élevé au carré et définir le p-value via la loi de Fisher a 1 et n-1 degrés de liberté. Ici, n correspond au nombre d’hôpitaux (46).
reg_2p<-lm(satis~procedure+avant+apres+factor(hospital)+factor(month),
data=dat2p)
Fish<-coeftest(reg_2p,vcov.=vcovCL(reg_2p,cluster=dat2p$hospital))[3,3]^2 %>%
round(digits = 8)
prob<-pf(Fish,1,45,lower.tail=FALSE)
c(F=Fish,pv=prob) %>% round(digits=4)
## F pv
## 0.5516 0.4615
rm(reg_2p,Fish,prob,dat2p)
Comme précédemment on ne peut pas rejeter H0 autrement-dit on ne peut pas rejeté l’hypothèse que \(\zeta_1\) soit nulle et donc qu’il n’y ait pas de différence de trend entre les futures traités et le groupe de contrôle. L’hypothèse de parallel trend pré traitement ne peut donc pas pas être rejetée.
Cela va dans le même sens que l’analyse graphique et notre test précédent.
Analyse graphique graphique de la temporalité du traitement
En plus de la parallel trend (ou comme autre test de cette dernière), on peut s’interroger sur la temporalité des effets du traitement. Est-ce que la date retenue est bien celle à partir de laquelle celui-ci produit des effets? Est-ce qu’il n’y a pas d’anticipation ou de retard ce qui pourrait remettre en cause (au moins en partie) le cadre méthodologique (la parallel trend serait en partie remise en cause)? Pour le voir, on peut présenter le graphe de l’effet du traitement dans le temps (comme nous l’avons fait dans le post précédent). Autrement-dit, nous procédons à l’étude des dynamic treatement effects.
Il s’agit d’estimer dans un seul modèle, pour une série de dates, l’effet du traitement sur les traités (même s’ils n’ont pas encore été traité) via une spécification de type 2x2 Diff in Diff . Cela se concrétise par une série d’interactions entre les différentes dates retenues et la variable binaire marquant le fait d’appartenir au groupe des traités. Les dates choisies correspondent (généralement retenu) aux périodes avant l’évènement à l’exception de celle le précédent immédiatement (qui sera notre référence) et de l’ensemble de celles après.
On a donc ici à estimer le modèle suivant:
\[ satis_{it}=\gamma_i+\gamma_t+\beta_1.T_{−2}.Traité+\beta_2.T_{−1}.Traité+\beta_3.T_1.Traité+\beta_4.T_2.Traité+\beta_5.T_3.Traité+\beta_6.T_4.Traité+\epsilon_{it} \]
où \(T_{−1}\) correspond à janvier, \(T_{−2}\) à février, \(T_1\) à avril, \(T_2\) à mai, etc…
Le traitement est attribué en avril, le mois 4, sur les 7 disponibles. Si on fixe la référence au mois 3, on a 2 leads (dans la mesure où ils correspondraient à des anticipations du traitement) et 4 lags (dans la mesure où ils correspondraient des effets retardés du traitement).
Pour mettre en oeuvre l’estimation, commençons par centrer nos dates autour de l’évènement en reprenant la base nous ayant permis d’estimer la 2x2 diff in diff qui comprend la variable Traite qui indique si l’observation est issue d’un hôpital qui adoptera ou a adopté le nouveau système. Établissons la variable comme un facteur pour permettre les interactions date par date.
dat3<-dat2_ %>%
mutate(m_cent3=month-3,
m_cent3f=relevel(factor(m_cent3),ref="0"))
head(dat3)
## # A tibble: 6 × 9
## # Groups: hospital [1]
## hospital frequency month procedure satis Traite post m_cent3 m_cent3f
## <dbl> <dbl+lbl> <dbl+lbl> <dbl+lbl> <dbl> <chr> <lgl> <dbl> <fct>
## 1 1 3 [High] 7 [July] 1 [New] 4.11 Trait… TRUE 4 4
## 2 1 2 [Medium] 3 [March] 0 [Old] 3.32 Trait… FALSE 0 0
## 3 1 4 [Very high] 2 [Febru… 0 [Old] 3.41 Trait… FALSE -1 -1
## 4 1 2 [Medium] 4 [April] 1 [New] 3.00 Trait… TRUE 1 1
## 5 1 1 [Low] 3 [March] 0 [Old] 3.11 Trait… FALSE 0 0
## 6 1 1 [Low] 7 [July] 1 [New] 2.88 Trait… TRUE 4 4
Ceci fait il ne nous reste plus qu’à établir la régression. Ici, compte tenu du grand nombre de coefficients qui seront calculés (58), nous utiliserons la fonction feols() du package fixest pour l’estimation. Il nous permet ici d’obtenir uniquement la série de coefficient qui nous intéresse. Présentons-les ainsi que les tests associés (toujours avec le clustering sur hospital).
reg_dyn <- feols(satis ~ m_cent3f*Traite |
hospital + month,
data = dat3)
## The variables 'm_cent3f-2', 'm_cent3f-1', 'm_cent3f1', 'm_cent3f2', 'm_cent3f3', 'm_cent3f4' and 'TraiteTraités' have been removed because of collinearity (see $collin.var).
coeftest(reg_dyn,vcov.=vcovCL(reg_dyn,cluster=dat3$hospital))%>%
round(digits = 7)
##
## t test of coefficients:
##
## Estimate Std. Error t value Pr(>|t|)
## m_cent3f-2:TraiteTraités 0.027897 0.035431 0.7874 0.4311
## m_cent3f-1:TraiteTraités 0.021732 0.037860 0.5740 0.5660
## m_cent3f1:TraiteTraités 0.822815 0.049302 16.6895 <2e-16 ***
## m_cent3f2:TraiteTraités 0.904050 0.046786 19.3230 <2e-16 ***
## m_cent3f3:TraiteTraités 0.844724 0.060565 13.9474 <2e-16 ***
## m_cent3f4:TraiteTraités 0.897888 0.050960 17.6193 <2e-16 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
On constate que dans leur ensemble les coefficient des leads ne sont pas statistiquement significatifs contrairement à l’ensemble des lags. Cela confirme l’absence d’anticipation du traitement. Produisons le graphe permettant de mettre en évidence ce résultat. Celui ce base sur la présentation des intervalles de confiance à 95% des coefficients des différents leads et lags. Ceux-ci peuvent simplement être obtenu à partir de la fonction coefci() du package lmtest.
coefci(reg_dyn,vcov.=vcovCL(reg_dyn,cluster=dat3$hospital),level=0.95)
## 2.5 % 97.5 %
## m_cent3f-2:TraiteTraités -0.04155824 0.09735225
## m_cent3f-1:TraiteTraités -0.05248483 0.09594913
## m_cent3f1:TraiteTraités 0.72617022 0.91946035
## m_cent3f2:TraiteTraités 0.81233554 0.99576416
## m_cent3f3:TraiteTraités 0.72599932 0.96344865
## m_cent3f4:TraiteTraités 0.79799120 0.99778581
Pour assembler le graphe ici nous avons plusieurs possibilités (comme dans le post précédent), on peut par exemple utiliser la fonction coefplot() du package fixest ou ggcoef() du package GGally. Nous optons pour la second solution qui, comme le nom l’indique, utilise la syntaxe de ggplot.
GGally::ggcoef(reg_dyn,
errorbar_height = 0.15,
errorbar_size = 1,
vline_linetype = "dashed",
vline_color = "black",
vline_size = 0.2)+
annotate(geom="point",x=0,y=2.5)+
labs(title="Effet dynamique du traitement",
subtitle = "intervalle de confiance à 95%",
x="Estimation DiD ",
y="Mois")+
scale_y_discrete(labels=c("janvier","fevrier","avril",
"mai","juin","juillet"))+
scale_x_continuous(breaks=seq(0,1,0.2))+
coord_flip()+
theme_minimal()+
theme(
plot.title = element_text(hjust = 0.5,face="bold"),
plot.subtitle = element_text(hjust = 0.5,face="italic"),
axis.line = element_line(color="black",
linewidth=0.15),
axis.ticks = element_line(color="black",
linewidth=0.1),
panel.grid.minor = element_blank()
)
## Registered S3 method overwritten by 'GGally':
## method from
## +.gg ggplot2
## Warning: The `exponentiate` argument is not supported in the `tidy()` method
## for `fixest` objects and will be ignored.
Test statistique de la temporalité du traitement En complément avec l’analyse graphique, on peut pratiquer un test de causalité de type test de Granger pour vérifier si les effets du traitement sont observés avant sa mise en oeuvre (officielle, la date d’évènement). Sous Stata, il est pratiqué en post estimation via la commande estat granger. Répliquons la.
Le test part de la même modélisation que celle que nous avons mobilisé pour générer le graphe du dynamic treatement effects à la différence prés que seul les leads sont considérés. Il s’agira alors de tester l’hypothèse jointe (H0) que les coefficients associés sont tous égaux à 0. Cela est réalisé à partir d’un test de Wald dont la statistique suit une loi de Fisher égale au nombre de leads et au nombre de cluster moins 1.
Avant de procéder au test, créons une variable indiquant les leads, la référence et les lags.
dat3<-dat3 %>%
mutate(leads_=ifelse(m_cent3%in%c(-2,-1,0),m_cent3f,"lags"),
leads_f=relevel(factor(leads_,labels = c("0","-2","-1","lags")),
ref="0"))
head(dat3)[,c(3,8:11)]
## # A tibble: 6 × 5
## month m_cent3 m_cent3f leads_ leads_f
## <dbl+lbl> <dbl> <fct> <chr> <fct>
## 1 7 [July] 4 4 lags lags
## 2 3 [March] 0 0 1 0
## 3 2 [February] -1 -1 3 -1
## 4 4 [April] 1 1 lags lags
## 5 3 [March] 0 0 1 0
## 6 7 [July] 4 4 lags lags
Une fois les données prêtes, il ne reste plus qu’à réaliser la régression. Observons-en les résultats.
reg_dyn_ <- feols(satis ~ leads_f*Traite |
hospital + month,
data = dat3)
## The variables 'leads_f-2', 'leads_f-1', 'leads_flags' and 'TraiteTraités' have been removed because of collinearity (see $collin.var).
coeftest(reg_dyn_,vcov.=vcovCL(reg_dyn_,cluster=dat3$hospital))%>%
round(digits = 7)
##
## t test of coefficients:
##
## Estimate Std. Error t value Pr(>|t|)
## leads_f-2:TraiteTraités 0.027897 0.035431 0.7874 0.4311
## leads_f-1:TraiteTraités 0.021732 0.037860 0.5740 0.5660
## leads_flags:TraiteTraités 0.867369 0.042337 20.4873 <2e-16 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Les deux coefficients associés aux leads ne sont pas statistiquement différents de zéro. Il ne semble pas y avoir d’anticiaption des effets du traitement. Néanmoins, pour aller au bout de la démarche, pratiquons le test d’hypothèse jointe sur ces coefficients (H0 les deux coefficients sont égale à 0). Pour cela, utilisons la fonction linearHypothesis() qui est issue du package car. Indiquons les hypothèses en tester et la nature de la statistique retenue (ici F pour Fisher).
linearHypothesis(reg_dyn_,c("leads_f-2:TraiteTraités = 0",
"leads_f-1:TraiteTraités = 0"),
test="F",
vcov.=vcovCL(reg_dyn_,cluster=dat3$hospital),
white.adjust="hc1")
##
## Linear hypothesis test:
## leads_f-2:TraiteTraités = 0
## leads_f-1:TraiteTraités = 0
##
## Model 1: restricted model
## Model 2: satis ~ leads_f * Traite | hospital + month
##
## Note: Coefficient covariance matrix supplied.
##
## Res.Df Df F Pr(>F)
## 1 7315
## 2 7313 2 0.3278 0.7205
rm(reg_dyn,reg_dyn_,dat3)
Le test confirme que l’on ne peut par raisonnablement rejeter H0. Cela va dans le sens d’une absence d’effets du traitement avant la date désignée comme étant celle de sa mise en place. L’hypothèse de non anticipation semble tenir.
Pour résumer, la mise en place automatisation de la procédure d’enregistrement des patients apparaît bien améliorer la satisfaction des patients. La méthode utilisée pour le mettre en évidence l’analyse en Diff in Diff (double différence) le confirme. Par ailleurs, les hypothèses sur lesquelles reposent cette méthode pour être mise en oeuvre parallel trend et non anticipation apparaissent également vérifiées.