Kapitel 62 Texte bereinigen

Wer mit Texten umgehen kann, erspart sich viel Zeit und Ärger

62.1 Diagnosen auslesen

Stellen wir uns vor, wir hätten eine schriftliche Befragung durchgeführt und die Patient:innen wurden nach Krankheiten gefragt. Sie gaben diese Krankheiten als freien Text in ein Feld ein.

Wir möchten nun diese Krankheiten auslesen.

Für das Beispiel erstellen wir zuerst ein paar Daten:

Diagnose<-c("Hezrproblem", "Hertzproblem","Hemiplegie", "Herzinfarkt", "Der Arzt meinte es sei etwas mit dem Herzen", "Herzkreislauferkrankung", "Lungenfibrose", "Lungenödem", "Angina Pectoris", "Enger Spinalkanal", "Infarkt", "Bypass", "Herzkranzgefässe", "herzinfaktr", "hetzinfarkt", "Rückenschmerzen", "Lumbago", "Bandscheibe", "Nackenschmerzen", "Herzinfarkt, Rückenschmerzen", "Herzprobleme, Schlaganfall", "Migraine, etwas mit dem Herzen", "Prostatakrebs", "bypass")

ID<-1:length(Diagnose)
data<-data.frame(ID, Diagnose)

Da die Studienteilnehmenden mehr als eine Krankheit einschreiben konnten, können wir nicht eine Variable erstellen, die alle Krankheiten beinhaltet (Das machen wir dann im zweiten Beispiel). Hier müssen wir für jede Krankheit eine Variable erstellen, die dann entweder 0 oder 1 als Kategorie hat.

Hier ein erster Versuch, der schon mal nicht so schlecht funktioniert - aber noch nicht perfekt ist. Wir erstellen ein Objekt, hier mit dem Namen wörter_die_herzprobleme_umschreiben (etwas langer Name, wir könnten es natürlich auch einfach herzproblem nennen). In dieses Objekt speichern wir alle Wortteile, die auf ein Herzproblem deuten könnten. Wir erstellen die Variable Herzproblem und eine bedingte Zuteilung des Wertes 1, wenn die Wortteile vorkommen. Dazu benutzen wir das stringr Paket Hier der Link zum Cheat-Sheet und hier der Link zu einem Beispiel mit einer Wortsuche.

Die bedingte Zuteilung machen wir mit der case_when Funktion.

Diese erste Variante funktioniert jedoch nicht gut:

wörter_die_herzproblem_umschreiben<-paste("herz", "Herz", "Pectoris", "Infarkt")

data<-data %>% 
  mutate(Herzprobleme=case_when(
    stringr::str_detect(Diagnose,wörter_die_herzproblem_umschreiben )~1))
DT::datatable(data)

Diese Lösung ist schon gar nicht schlecht. Aber es gibt damit öfters komische Probleme. Wenn wir zum Beispiel noch das Wort Bypass hinzufügen, funktioniert das ganze nicht mehr. Keine Ahnung warum (Wer weiss warum, soll doch bitte mir unten in den Chat schreiben - den Chat findet ihr unten rechts auf der Seite im blauen Kreis.)

Hier sehen wir, dass mit dem Hinzufügen von *Bypass” nicht mehr alle Wörter erkannt werden.

wörter_die_herzproblem_umschreiben<-c("herz", "Herz", "Pectoris", "Infarkt", "Bypass")

data<-data %>% 
  mutate(Herzprobleme=case_when(
    stringr::str_detect(Diagnose,wörter_die_herzproblem_umschreiben )~1
  ))
## Error in `mutate()`:
## ! Problem while computing `Herzprobleme = case_when(...)`.
## Caused by error in `stringr::str_detect()`:
## ! Can't recycle `string` (size 24) to match `pattern` (size 5).
DT::datatable(data)

Die folgende Lösung, etwas komplizierter, funktioniert jedoch einwandfrei:

Dank geht hier an Sophie Carrard, die mich auf diese Lösung gebracht hat.

wörter_die_herzproblem_umschreiben<-paste("Herz", "Pectoris", "Infarkt", "Bypass", sep="|")

data<-data %>% 
  mutate(Herzprobleme=case_when(
    stringr::str_detect(Diagnose,wörter_die_herzproblem_umschreiben )~1
  ))
DT::datatable(data)

Nun können wir aber die Lösung noch etwas eleganter gestalten. Zum Beispiel mussten wir jetzt immer Gross- und Kleinschreibung berücksichtigen - was wir in unserer Lösung nicht konsequent getan haben. Wenn nämlich ein:e Patient:in bypass klein geschrieben hätte, würde unsere Lösung dies nicht erkennen.

Die elegante Lösung dafür ist, dass wir beide Seiten, d.h. die Textinhalte in der Variable Diagnose und die Inhalte in unserer Suche zuerst gänzlich auf klein umschalten. Dies können wir mit der Funktion tolower oder stringr::str_to_lower tun. Ehrlich gesagt, habe ich noch nicht herausgefunden, wann die stringr::str_to_lower Vorteile bringt. Hier eine Webseite, die Unterschiede zwischen base R (z.B. tolower ist base R) und stringr (tidyverse, zum Beispiel str_to_lower) aufzeigt.

wörter_die_herzproblem_umschreiben<-tolower(paste("herz", "Herz", "Pectoris", "Infarkt", "Bypass", sep="|")) # Variante 1

wörter_die_herzproblem_umschreiben<-stringr::str_to_lower(paste("herz", "Herz", "Pectoris", "Infarkt", "Bypass", sep="|")) # Variante 2

data<-data %>% 
  mutate(Herzprobleme=case_when(    stringr::str_detect(stringr::str_to_lower(Diagnose),wörter_die_herzproblem_umschreiben)~1,
  TRUE~0))
DT::datatable(data)

Nun haben wir noch ein Problem mit den falsch geschriebenen “Herzen”, zum Beispiel schreibt jemand “hetzinfarkt”. Entweder überprüfen wir dies manuell, oder wir sind kreativ und überlegen, wie wir solche Probleme umgehen können. Eine kreative Lösung wäre folgende Lösung: Wir benutzen einerseitzs “he.+z”, wobei das .+ sagt: . = irgendein Buchstabe, und + bedeutet “Ein Buchstabe oder mehr”. Diese Variante findet nur auch den Fehler von ID 2. Für ID müssen wir noch folgendes anfügen: “^He.+[rz]”, dies findet alle Wörte die mit he beginnen (gross oder klein geschrieben), plus die die dann noch die Buchstaben r und z im Wort haben. So wird zum Beispiel Hemiplegie, das auch mit He beginnt, nicht gefunden - was für uns gut ist.

wörter_die_herzproblem_umschreiben<-stringr::str_to_lower(paste("he.+z","^He.+[rz]", "Pectoris", "Infarkt", "Bypass", sep="|")) 

data<-data %>% 
  mutate(Herzprobleme=case_when(stringr::str_detect(stringr::str_to_lower(Diagnose),wörter_die_herzproblem_umschreiben)~1,
  TRUE~0))
DT::datatable(data)

Jetzt können wir analog mit den anderen Diagnose verfahren - das zeigen wir jetzt hier nicht, sollte jedoch selbsterklärend sein.

Beim nächsten Beispiel mussten die Teilnehmenden ihr Hauptproblem angeben, sie konnten also nur eine Diagnose angeben.

Wir erstellen wieder ein paar fiktive Daten:

Diagnose<-c("Hezrproblem", "Hertzproblem","Hemiplegie", "Herzinfarkt", "Der Arzt meinte es sei etwas mit dem Herzen", "Herzkreislauferkrankung", "Lungenfibrose", "Lungenödem", "Angina Pectoris", "Enger Spinalkanal", "Infarkt", "Bypass", "Herzkranzgefässe", "herzinfaktr", "hetzinfarkt", "Rückenschmerzen", "Lumbago", "Bandscheibe", "Nackenschmerzen", "Rückenschmerzen", "Herzprobleme, Schlaganfall", "Migraine", "Prostatakrebs", "bypass")

ID<-1:length(Diagnose)
data<-data.frame(ID, Diagnose)

Hier ein Lösungsvorschlag:

wörter_die_herzproblem_umschreiben<-stringr::str_to_lower(paste("he.+z","^He.+[rz]", "Pectoris", "Infarkt", "Bypass", sep="|")) 
wörter_die_muskuloskelettale_probleme_umschreiben<-stringr::str_to_lower(paste("^R.cken","Nacken", "Band", "Schme[rz]", "Spinal", "Lum", sep="|")) 
wörter_die_neurologische_krankheiten_umschreiben<-stringr::str_to_lower(paste("Schlag","Hirn", "Hemi", "Plegi", sep="|")) 
wörter_die_respiratorische_krankheiten_umschreiben<-stringr::str_to_lower(paste("Lunge","Pneu", "Pulm", "Ate", sep="|")) 

data<-data %>% 
  mutate(Diagnosen=case_when(stringr::str_detect(stringr::str_to_lower(Diagnose),wörter_die_herzproblem_umschreiben)~"Herzkreislauf",
                                   stringr::str_detect(stringr::str_to_lower(Diagnose),wörter_die_respiratorische_krankheiten_umschreiben)~"Atemwege",
                                   stringr::str_detect(stringr::str_to_lower(Diagnose),wörter_die_muskuloskelettale_probleme_umschreiben)~"Bewegungsapparat",
                                   stringr::str_detect(stringr::str_to_lower(Diagnose),wörter_die_neurologische_krankheiten_umschreiben)~"Neurologisch", 
                                    TRUE~"Andere"))
DT::datatable(data)
summarytools::freq(data$Diagnosen, order='freq')
## Frequencies  
## data$Diagnosen  
## Type: Character  
## 
##                          Freq   % Valid   % Valid Cum.   % Total   % Total Cum.
## ---------------------- ------ --------- -------------- --------- --------------
##          Herzkreislauf     13     54.17          54.17     54.17          54.17
##       Bewegungsapparat      6     25.00          79.17     25.00          79.17
##                 Andere      2      8.33          87.50      8.33          87.50
##               Atemwege      2      8.33          95.83      8.33          95.83
##           Neurologisch      1      4.17         100.00      4.17         100.00
##                   <NA>      0                               0.00         100.00
##                  Total     24    100.00         100.00    100.00         100.00

62.2 E-Mail auslesen

E-Mail Adressen aus einem Text auslesen ist einfach:

text<-c("*Correspondence: sophie.carrard@hevs.ch 1 School of Health Sciences, Physiotherapy, HES-SO Valais-Wallis, 
Rathausstrasse 25, 3954 Leukerbad, Switzerland", "1School of Health Sciences, HES-SO Valais-Wallis, Leukerbad, Switzerland
2School of Health Sciences (HESAV), University of Applied Sciences and Arts Western Switzerland (HES-SO), Lausanne, Switzerland
3Rehazentrum Valens - Kliniken Valens, Taminaplatz 1, 7317 Valens, Switzerland
4Department of Rehabilitation Sciences, KU Leuven - University of Leuven, Tervuursevest 101, 3001 Leuven, Heverlee Belgium
Karl Martin Sattelmayer, * Correspondence: martin.sattelmayer@hevs.ch 1
School of Health Sciences, HES-SO Valais-Wallis, Leukerbad, Switzerland
Full list of author information is available at the end of the ", "1 UVSQ, Erphan, Paris-Saclay University, 78000 Versailles, France
2
Intensive Care Unit Department, Le Havre Hospital, Avenue Pierre Mendes France,
76290 Montivilliers, France; laurie.lagache@ch-havre.fr
3 Saint Michel School of Physiotherapy, Paris-Saclay University, 75015 Paris, France
4 Department of Physiotherapy, Erasme University Hospital, Université Libre de Bruxelles,
1070 Bruxelles, Belgium; Alexis.Gillet@erasme.ulb.ac.be (A.G.); olivier.van.hove@erasme.ulb.ac.be (O.V.H.);
michelle.norrenberg@erasme.ulb.ac.be (M.N.)
5
Inserm U1096, Rouen University Hospital, 76000 Rouen, France; fairuz.boujibar@chu-rouen.fr
6 Department of General and Thoracic Surgery, Rouen University Hospital, 76000 Rouen, France
7 Physiotherapy Unit, RHNe Réseau Hospitalier Neuchâtelois, Pourtalès Hospital,
2000 Neuchatel, Switzerland; jonathan.dugernier@rhne.ch (J.D.); nils.correvon@hesav.ch (N.C.)
8
Institute of Social & Preventive Medicine, University of Bern, 3012 Bern, Switzerland;
marcel.zwahlen@ispm.unibe.ch
9
Institute for Research and Innovation in Biomedicine (IRIB), Normandie Univ, UNIROUEN, EA3830-GRHV,
76000 Rouen, France; bouchra.lamia@chu-rouen.fr (B.L.); christophe.girault@chu-rouen.fr (C.G.);
laurence.haesler@rhne.ch (L.H.); Elise.Artaud-Macari@chu-rouen.fr (E.A.M.); gprieur.kine@gmail.com (G.P.);
yann.combret@gmail.com (Y.C.)
10 Pulmonology Department, Le Havre Hospital, Avenue Pierre Mendes France, 76290 Montivilliers, France;
laure.goubert@ch-havre.fr
11 Pulmonology, Respiratory Department, Rouen University Hospital, 76000 Rouen, France
12 Intensive Care Unit Department, Rouen University Hospital, 76000 Rouen, France;
Philippe.gouin@chu-rouen.fr
13 Department of Intensive Care, Erasme University Hospital, Université Libre de Bruxelles,
1070 Bruxelles, Belgium; jacques.creteur@erasme.ulb.ac.be
14 Pulmonary Medicine Unit, RHNe Réseau Hospitalier Neuchâtelois, Pourtalès Hospital,
2000 Neuchatel, Switzerland; Jean-Marc.Fellrath@rhne.ch
15 Intensive Care Unit, RHNe Réseau Hospitalier Neuchâtelois, Pourtalès Hospital,
2000 Neuchatel, Switzerland
16 Pulmonary, Thoracic Oncology and Respiratory Intensive Care Department, Rouen University Hospital,
76000 Rouen, France
17 Department of Pneumology, Erasme University Hospital, Université Libre de Bruxelles,
1070 Bruxelles, Belgium; olivier.taton@erasme.ulb.ac.be (O.T.); dimitri.leduc@erasme.ulb.ac.be (D.L.)
18 Research and Clinical Experimentation Institute (IREC), Pulmonology, ORL and Dermatology, Louvain
Catholic University, 1200 Brussels, Belgium
19 School of Health Sciences (HESAV), HES-SO University of Applied Sciences and Arts Western Switzerland,
1011 Lausanne, Switzerland; olivier.contal@hesav.ch
20 School of Health Sciences, University of Applied Sciences and Arts Western Switzerland Valais (HES-SO
Valais-Wallis), 3954 Leukerbad, Switzerland; roger.hilfiker@gmail.com
* Correspondence: medrinal.clement.mk@gmail.com")

id<-1:length(text)
data<-data.frame(id, text)

data<-data %>% 
  mutate(email=str_extract_all(text, "\\S*@\\S*"))
DT::datatable(data)

62.3 Telefonnummern auslesen

Auch Telefonnummern auslesen ist relativ einfach, man muss einfach den Text im str_detect Teil anpassen an die Formate, die im Text vorkommen. Siehe hier.

62.4 Name und Vorname umdrehen

Firstname=c("John", "Paul", "George", "Ringo")
Lastname=c("Lennon", "McCartney", "Harrison", "Starr")
data<-data.frame(Firstname, Lastname)

data<-data %>% 
    mutate(Firstname_Lastname=paste(Firstname, Lastname, " "))

head(data)
##   Firstname  Lastname Firstname_Lastname
## 1      John    Lennon      John Lennon  
## 2      Paul McCartney   Paul McCartney  
## 3    George  Harrison  George Harrison  
## 4     Ringo     Starr      Ringo Starr
data<-data %>% 
  mutate(Lastname_Firstname=str_replace(Firstname_Lastname, "(\\w+)( )(\\w+)","\\3 \\2 \\1"))

62.5 Elemente aus E-Mail auslesen

Im nächsten Beispiel lesen wir mit dem Paket stringr Elemente aus einer E-Mail Adresse raus.

email <- c("rotscher.himmelflicker@gmail.com", "rotscher.hilfi@gmail.com", "rutscher.hilfiger@gmail.com", "r.hilfi@gmail.com")
data<-data.frame(email)

# There would be a shorter way using regex
data_1<-data %>% 
  mutate(Firstname = str_split(email, "\\.", simplify = TRUE)[, 1]) %>% 
  mutate(Lastname = str_split(email, "\\.", simplify = TRUE)[, 2]) %>%
  mutate(Lastname = str_split(Lastname, "@", simplify = TRUE)[, 1]) %>% 
  mutate(provider = str_split(email, "@", simplify = TRUE)[, 2])

62.6 Punktschätzer und Limiten der Konfidenzintervalle auslesen

data<-rio::import("beispiel_extrahieren_schaetzer_konfidenzintervalle.xlsx")

data<-janitor::clean_names(data)
names(data)
## [1] "sn"   "sp"   "ppv"  "npv"  "lr"   "lr_2"
data<-data %>% 
  rename(lr_plus=lr, 
         lr_minus=lr_2)

So sehen die Daten aus, die wir extrahieren wollen:

DT::datatable(data, options=list(pageLength=10, scrollY = "800"), fillContainer = TRUE)

Es ist einfacher, die Angaben für das Auslesen in Objekten zu speichern. Je nachdem wie die Daten eingegeben wurden, muss man diese drei Objekte extract_point_estimate, extract_lower_boundary_confidence_interval und extract_upper_boundary_confidence_interval natürlich anpassen.

extract_point_estimate<-paste("(\\d)+(\\.)(\\d)+", "(\\d)+", sep="|")
extract_lower_boundary_confidence_interval<-paste("(?<=\\()[0-9]+(\\.)(\\d)+", "(?<=\\[\\s)[0-9]+(\\.)(\\d)+", "(?<=\\s)[0-9]+(\\.)(\\d)+", sep="|")
extract_upper_boundary_confidence_interval<-paste("(?<=\\-)[0-9]+(\\.)(\\d)+", "(?<=\\-\\s)[0-9]+(\\.)(\\d)+","(?<=%)[0-9]+(\\.)(\\d)+", sep="|")

So sieht der Code aus, der die Daten extrahiert. Natürlich hätten wir alles ein eine mutate Klammer einfügen können (d.h. es ist nicht nötig, jedesmal ein neues mutate zu schreiben). So wie es hier ist, ist es jedoch einfacher Fehler zu finden - und es war auch einfacher um den code mit Copy-Paste zu erstellen.

data<-data %>% 
  mutate(sensitivity=str_extract(sn, extract_point_estimate)) %>% 
  mutate(specificity=str_extract(sp, extract_point_estimate)) %>% 
  mutate(PPV=str_extract(ppv, extract_point_estimate)) %>%
  mutate(NPV=str_extract(npv, extract_point_estimate)) %>%
  mutate(LR_plus=str_extract(lr_plus, extract_point_estimate)) %>%
  mutate(LR_minus=str_extract(lr_minus, extract_point_estimate)) %>%
  mutate(lb_sensitivity=str_extract(sn, extract_lower_boundary_confidence_interval))%>% 
  mutate(lb_specificity=str_extract(sp, extract_lower_boundary_confidence_interval)) %>% 
  mutate(lb_PPV=str_extract(ppv, extract_lower_boundary_confidence_interval)) %>%
  mutate(lb_NPV=str_extract(npv, extract_lower_boundary_confidence_interval)) %>% 
  mutate(lb_LR_plus=str_extract(lr_plus, extract_lower_boundary_confidence_interval)) %>% 
  mutate(lb_LR_minus=str_extract(lr_minus, extract_lower_boundary_confidence_interval)) %>% 
  mutate(ub_sensitivity=str_extract(sn, extract_upper_boundary_confidence_interval))%>% 
  mutate(ub_specificity=str_extract(sp, extract_upper_boundary_confidence_interval)) %>% 
  mutate(ub_PPV=str_extract(ppv, extract_upper_boundary_confidence_interval)) %>%
  mutate(ub_NPV=str_extract(npv, extract_upper_boundary_confidence_interval)) %>% 
  mutate(ub_LR_plus=str_extract(lr_plus, extract_upper_boundary_confidence_interval)) %>% 
  mutate(ub_LR_minus=str_extract(lr_minus, extract_upper_boundary_confidence_interval)) %>% 
  dplyr::select(sensitivity,lb_sensitivity,ub_sensitivity,  specificity, lb_specificity,
                ub_specificity,  PPV,lb_PPV,ub_PPV,   NPV,lb_NPV,ub_NPV,  LR_plus,
                lb_LR_plus,ub_LR_plus, LR_minus,lb_LR_minus,ub_LR_minus,  everything() )

Und hier das Ergebnis:

DT::datatable(data, options=list(pageLength=10, scrollY = "800"), fillContainer = TRUE)

62.7 Viele Variablennamen auf einmal ändern

Dieser_Alte_Hase=rnorm(10,2,2)
Dieses_Alte_Pferd=rnorm(10,4,2)
Dieses_Alte_Haus=rnorm(10,4,2)

data<-data.frame(Dieser_Alte_Hase, Dieses_Alte_Pferd, Dieses_Alte_Haus)
names(data)
## [1] "Dieser_Alte_Hase"  "Dieses_Alte_Pferd" "Dieses_Alte_Haus"
head(data)
##   Dieser_Alte_Hase Dieses_Alte_Pferd Dieses_Alte_Haus
## 1       -1.6102281          2.611742         4.439085
## 2        3.7613009          2.103681         2.059042
## 3        0.8060786          1.700717         4.233055
## 4        0.1962261          3.799007         4.460152
## 5        2.6376875          2.468576         1.103747
## 6        3.7761373          3.518885         1.786545
data<-data %>% 
  rename_all(~str_replace(., "_Alte_", ".Junge."))

names(data)  
## [1] "Dieser.Junge.Hase"  "Dieses.Junge.Pferd" "Dieses.Junge.Haus"
head(data)
##   Dieser.Junge.Hase Dieses.Junge.Pferd Dieses.Junge.Haus
## 1        -1.6102281           2.611742          4.439085
## 2         3.7613009           2.103681          2.059042
## 3         0.8060786           1.700717          4.233055
## 4         0.1962261           3.799007          4.460152
## 5         2.6376875           2.468576          1.103747
## 6         3.7761373           3.518885          1.786545

Wörter haben Macht