Vorbemerkungen

Dieses Dokument beschreibt die Vorprozessierung und explorative Analyse des Datensatzes, der Grundlage des auf srf.ch veröffentlichten Artikel Diese Telekom-Anbieter sorgen für den meisten Unmut ist.

SRF Data legt Wert darauf, dass die Datenvorprozessierung und -Analyse nachvollzogen und überprüft werden kann. SRF Data glaubt an das Prinzip offener Daten, aber auch offener und nachvollziehbarer Methoden. Zum anderen soll es Dritten ermöglicht werden, auf dieser Vorarbeit aufzubauen und damit weitere Auswertungen oder Applikationen zu generieren.

Die Endprodukte des vorliegenden Scripts, neben der vorliegenden explorativen Analyse, sind (Datenbeschreibung siehe unten):

  • reasons_top_four_providers.csv
  • cases_per_type.csv
  • cases_top_4_with_exit.csv

R-Script & Daten

Die Vorprozessierung und Analyse wurde im Statistikprogramm R vorgenommen. Das zugrunde liegende Script sowie die prozessierten Daten können unter diesem Link heruntergeladen werden. Durch Ausführen von main.Rmd kann der hier beschriebene Prozess nachvollzogen und der für den Artikel verwendete Datensatz generiert werden. Dabei werden Daten aus dem Ordner input eingelesen und Ergebnisse in den Ordner output geschrieben.

SRF Data verwendet das rddj-template von Timo Grossenbacher als Grundlage für seine R-Scripts. Entstehen bei der Ausführung dieses Scripts Probleme, kann es helfen, die Anleitung von rddj-template zu studieren.

Debug-Informationen: This report was generated on 2019-11-19 14:12:02. R version: 3.5.3 on x86_64-pc-linux-gnu. For this report, CRAN packages as of 2019-03-01 were used.

GitHub

Der Code für die vorliegende Datenprozessierung ist auf https://github.com/srfdata/2019-11-ombudscom zur freien Verwendung verfügbar.

Weitere Projekte

Code & Daten von SRF Data sind unter https://srfdata.github.io verfügbar.

Haftungsausschluss

Die veröffentlichten Informationen sind sorgfältig zusammengestellt, erheben aber keinen Anspruch auf Aktualität, Vollständigkeit oder Richtigkeit. Es wird keine Haftung übernommen für Schäden, die durch die Verwendung dieses Scripts oder der daraus gezogenen Informationen entstehen. Dies gilt ebenfalls für Inhalte Dritter, die über dieses Angebot zugänglich sind.

Datenbeschreibung

cases_per_type.csv

Attribut Typ Beschreibung
year Numeric Jahr der Beobachtung
Fernmeldedienst Numeric Anzahl Fälle der Fernmeldedienstanbieter
Mehrwertdienst Numeric Anzahl Fälle der Mehrwertdienstanbieter

cases_top_4_with_exit.csv

Exportiert werden nur die drei aus unserer Sicht wichtigsten Ausgänge. Andere bezieht sich sowohl auf Fernmeldedienstanbieter wie auch auf Mehrwertdienstanbieter, auch hier sind die Jahre 2014-18 abgebildet.

Attribut Typ Beschreibung
category String Ausgang des Falls in Worten
Salt Numeric Anzahl Fälle 2014-2018 mit diesem Ausgang bei Salt
Sunrise Numeric Anzahl Fälle 2014-2018 mit diesem Ausgang bei Sunrise
Swisscom Numeric Anzahl Fälle 2014-2018 mit diesem Ausgang bei Swisscom
UPC Numeric Anzahl Fälle 2014-2018 mit diesem Ausgang bei UPC
Andere Numeric Anzahl Fälle 2014-2018 mit diesem Ausgang bei allen anderen Providern

reasons_top_four_providers.csv

Diese Zahlen beziehen sich nur auf die Fälle der 4 grössten Provider Salt, Sunrise, Swisscom und UPC, auch hier sind die Jahre 2014-18 abgebildet.

Attribut Typ Beschreibung
category String Beschwerdegrund
value Numeric Anzahl Fälle 2014-2018 mit diesem Beschwerdegrund

Originalquelle

Originalquelle der Auswertungen sind Daten, die SRF im Rahmen des Öffentlichkeitsprinzips von der Ombudscom herausverlangt hat. Die betreffenden Excel-Dateien, die SRF zugeschickt worden waren, finden sich im Ordner input/data.

Lesen Sie mehr über das Verfahren eines Ombudscom-Falls auf der betreffenden Website oder im letzten Bericht (2018). Wichtig ist uns die Unterscheidung zwischen einer Anfrage und einem Fall. Es wird wie folgt beschrieben: “Eine Anfrage wird statistisch erfasst, wenn sich eine Person schriftlich oder telefonisch an die Schlichtungsstelle wendet und die Voraussetzungen für eine Einleitung des Schlichtungsverfahrens gemäss Art. 8 Prozess- und Gebührenreglement noch nicht erfüllt sind.”

Das bedeutet, dass sich diese Analyse hauptsächlich auf die Fälle und nicht auf die Anfragen konzentriert. So wurden beispielsweise im Jahr 2018 die folgenden Untergruppen von Anfragen protokolliert:

  • 564 abgeschlossen, weil sie zu einem Fall wurden
  • 872 Abschluss infolge nicht eingereichter Dokumente
  • 238 Rückzug der Anfrage
  • 101 Unzuständigkeit
  • 1344 telefonische Anfragen betreffend Informationen/Ratschläge
  • 184 schriftliche Anfragen betreffend Informationen/Ratschläge

Wie im Jahresbericht 2018 auf Seite 9 angegeben. Im Erklärungstext ist zu verstehen, dass die Anfragen allein vielleicht keine sehr zuverlässige Informationsquelle sind: “872 abgeschlossene Anfragen sind auf nicht eingereichte Dokumente zurückzuführen. Diese machten knapp 26% aller Anfragen aus. Gegenüber dem Vorjahr stieg dieser Wert leicht. Oft sind den Kundinnen und Kunden die Hürden zur Durchführung eines Schlichtungsverfahrens zu hoch. Bereits das Ausfüllen eines speziell für das Schlichtungsverfahren vorgesehenen Formulars bereitete einigen Personen Mühe. Oftmals gingen sie fälschlicherweise auch davon aus, dass der Ombudsmann ihre Interessen gegenüber den Anbietern vertritt oder die streitige Angelegenheit umgehend (z.B. mit einem Telefonat mit dem betroffenen Anbieter) lösen kann. Auch wurden sie von auf die Schlichtungsstelle verweisenden Stellen falsch über die Funktion und die Aufgaben der Schlichtungsstelle informiert. Die Mitarbeitenden der Schlichtungsstelle klärten die Kundinnen und Kunden über die Tätigkeit auf und informierten sie über die Eintretensvoraussetzungen: Bevor ein Schlichtungsverfahren eingeleitet werden kann, muss die begehrende Partei das Formular „Schlichtungsbegehren“ unter Angabe des Sachverhalts und Ziels ausfüllen sowie glaubhaft darlegen, dass sie sich innerhalb der letzten 12 Monate erfolglos um eine Lösung des Problems bemüht hatte. Ging aus der Anfrage nicht hervor, dass die Kundin oder der Kunde bereits eine Lösung mit dem Anbieter angestrebt hatte, empfahl die Schlichtungsstelle, sich schriftlich mit einer Beschwerde an den betroffenen Anbieter zu wenden. Dieser Aufwand war vielen Kundinnen und Kunden zu gross. Sie gingen – wie bereits erwähnt – davon aus, dass die Schlichtungsstelle ihre Interessen vertreten könne und sie in der Angelegenheit nichts weiter unternehmen müssen. Daher strebten sie kein Schlichtungsverfahren an und die Anfragen wurden infolge nicht eingereichter Dokumente abgeschlossen.”

Vorbereitungen

## [1] "package package:rmarkdown detached"
## Loading required package: knitr
## Loading required package: rstudioapi

Packages definieren

# from https://mran.revolutionanalytics.com/web/packages/checkpoint/vignettes/using-checkpoint-with-knitr.html
# if you don't need a package, remove it from here (commenting is probably not sufficient)
# tidyverse: see https://blog.rstudio.org/2016/09/15/tidyverse-1-0-0/
cat("
library(rstudioapi)
library(tidyverse) # ggplot2, dplyr, tidyr, readr, purrr, tibble
library(glue) # string literals
library(magrittr) # pipes
library(readxl) # excel
library(scales) # scales for ggplot2
library(jsonlite) # json
library(lintr) # code linting
library(ggrepel) # repelling geom_text
library(rmarkdown)",
file = "manifest.R")

Packages installieren

# if checkpoint is not yet installed, install it (for people using this
# system for the first time)
if (!require(checkpoint)) {
  if (!require(devtools)) {
    install.packages("devtools", repos = "http://cran.us.r-project.org")
    require(devtools)
  }
  devtools::install_github("RevolutionAnalytics/checkpoint",
                           ref = "v0.3.2", # could be adapted later,
                           # as of now (beginning of July 2017
                           # this is the current release on CRAN)
                           repos = "http://cran.us.r-project.org")
  require(checkpoint)
}
# nolint start
if (!dir.exists("~/.checkpoint")) {
  dir.create("~/.checkpoint")
}
# nolint end
# install packages for the specified CRAN snapshot date
checkpoint(snapshotDate = package_date,
           project = path_to_wd,
           verbose = T,
           scanForPackages = F,
           use.knitr = F,
           R.version = R_version)
rm(package_date)

Packages laden

source("manifest.R")
unlink("manifest.R")
sessionInfo()
## R version 3.5.3 (2019-03-11)
## Platform: x86_64-pc-linux-gnu (64-bit)
## Running under: Ubuntu 18.04.3 LTS
## 
## Matrix products: default
## BLAS: /opt/R/R-3.5.3/lib/R/lib/libRblas.so
## LAPACK: /opt/R/R-3.5.3/lib/R/lib/libRlapack.so
## 
## locale:
##  [1] LC_CTYPE=en_US.UTF-8       LC_NUMERIC=C              
##  [3] LC_TIME=de_CH.UTF-8        LC_COLLATE=en_US.UTF-8    
##  [5] LC_MONETARY=de_CH.UTF-8    LC_MESSAGES=en_US.UTF-8   
##  [7] LC_PAPER=de_CH.UTF-8       LC_NAME=C                 
##  [9] LC_ADDRESS=C               LC_TELEPHONE=C            
## [11] LC_MEASUREMENT=de_CH.UTF-8 LC_IDENTIFICATION=C       
## 
## attached base packages:
## [1] stats     graphics  grDevices utils     datasets  methods   base     
## 
## other attached packages:
##  [1] rmarkdown_1.11   ggrepel_0.8.0    lintr_1.0.3      jsonlite_1.6    
##  [5] scales_1.0.0     readxl_1.3.0     magrittr_1.5     glue_1.3.0      
##  [9] forcats_0.4.0    stringr_1.4.0    dplyr_0.8.0.1    purrr_0.3.0     
## [13] readr_1.3.1      tidyr_0.8.2      tibble_2.0.1     ggplot2_3.1.0   
## [17] tidyverse_1.2.1  checkpoint_0.4.0 rstudioapi_0.9.0 knitr_1.21      
## 
## loaded via a namespace (and not attached):
##  [1] Rcpp_1.0.0       cellranger_1.1.0 plyr_1.8.4       pillar_1.3.1    
##  [5] compiler_3.5.3   tools_3.5.3      digest_0.6.18    lubridate_1.7.4 
##  [9] evaluate_0.13    nlme_3.1-137     gtable_0.2.0     lattice_0.20-38 
## [13] pkgconfig_2.0.2  rlang_0.3.1      rex_1.1.2        cli_1.0.1       
## [17] yaml_2.2.0       haven_2.1.0      xfun_0.5         withr_2.1.2     
## [21] xml2_1.2.0       httr_1.4.0       hms_0.4.2        generics_0.0.2  
## [25] grid_3.5.3       tidyselect_0.2.5 R6_2.4.0         modelr_0.1.4    
## [29] backports_1.1.3  htmltools_0.3.6  rvest_0.3.2      assertthat_0.2.0
## [33] colorspace_1.4-0 stringi_1.3.1    lazyeval_0.2.1   munsell_0.5.0   
## [37] broom_0.5.1      crayon_1.3.4
rm(list = ls(all = TRUE))

Daten einlesen

# prepare multiple tibbles that will be filled with data
status <- tibble() # Statistik
reasons_cases_requests <- tibble() # Beschwerdegründe (Fälle und Anfragen)
reasons_cases <- tibble() # Beschwerdegründe (nur Fälle)
reasons_requests <- tibble() # Beschwerdegründe (nur Anfragen)
languages <- tibble() # Sprache Beschwerdeführer
appellant_types <- tibble() # Art Beschwerdeführer
avg_sums <- tibble() # Durchschnittliche Streitsummen
avg_durations <- tibble() # Durchschnittlichen Behandlungsdauer

# the files have a consistent structure
c("FDA", "MWDA") %>%
  walk(function(current_type) {
    list.files(
      "input/data",
      pattern = glue("^\\d{{4}}_{current_type}.xlsx$"),
      full.names = TRUE
    ) %>%
      walk(function(current_file) {
        # create helper function that gathers providers
        # renames first column to category and appends info about year/file
        add_info_and_gather <- function(df) {
          df %>%
            rename(category = 1) %>%
            mutate(
              year = as.numeric(str_extract(current_file, "\\d{4}")),
              type = str_replace_all(current_type, c(
                "FDA" = "Fernmeldedienst",
                "MWDA" = "Mehrwertdienst"
              ))
            ) %>%
            gather(
              key = "provider",
              value = "value",
              -one_of("category", "year", "type", "file")
            ) %>%
            # convert to numeric, but first remove dots (thousands) and
            # replace commas (decimal) with point
            mutate(
              value = str_replace_all(value, "\\.", ""),
              value = str_replace_all(value, ",", "."),
              value = as.numeric(value)
            )
        }

        # read ranges and bind to prepared tibbles
        suppressMessages({
          status <<- status %>%
            bind_rows(
              read_excel(current_file, skip = 1, n_max = 37) %>%
                add_info_and_gather()
            )

          reasons_cases_requests <<- reasons_cases_requests %>%
            bind_rows(
              read_excel(current_file, skip = 40, n_max = 22) %>%
                add_info_and_gather()
            )

          reasons_cases <<- reasons_cases %>%
            bind_rows(
              read_excel(current_file, skip = 65, n_max = 22) %>%
                add_info_and_gather()
            )

          reasons_requests <<- reasons_requests %>%
            bind_rows(
              read_excel(current_file, skip = 90, n_max = 22) %>%
                add_info_and_gather()
            )

          languages <<- languages %>%
            bind_rows(
              read_excel(current_file, skip = 115, n_max = 4) %>%
                add_info_and_gather()
            )

          appellant_types <<- appellant_types %>%
            bind_rows(
              read_excel(current_file, skip = 122, n_max = 3) %>%
                add_info_and_gather()
            )

          avg_sums <<- avg_sums %>%
            bind_rows(
              read_excel(current_file, skip = 128, n_max = 2) %>%
                add_info_and_gather()
            )

          avg_durations <<- avg_durations %>%
            bind_rows(
              read_excel(current_file, skip = 133, n_max = 8) %>%
                add_info_and_gather()
            )
        })
      })
  })

# In our tables we still have some totals. Not only in the column category
# where it is the sum of all other categories, but also in the column provider
# we remove all these, we can sum stuff on our own perfectly well.
# Also convert category, type, provider to factor in all tibbles, don't be
# confused by the syntax with walk / assign, it's really just a fancy way
# to apply the same mutation to all variables named in the vector.
c(
  "status",
  "reasons_cases_requests",
  "reasons_cases",
  "reasons_requests",
  "languages",
  "appellant_types",
  "avg_sums",
  "avg_durations"
) %>%
  walk(function(current_tibble) {
    # create new variables with names …_totals
    assign(
      glue("{current_tibble}_totals"),
      get(current_tibble) %>%
        # now remove totals from original tables
        filter(
          str_detect(category, "^Total") |
          str_detect(provider, "^Total")
        ),
      envir = .GlobalEnv
    )
    # apply mutation to all variables
    assign(
      current_tibble,
      get(current_tibble) %>%
        # now remove totals from original tables
        filter(
          !str_detect(category, "^Total") &
          !str_detect(provider, "^Total")
        ) %>%
        mutate_at(
          vars(category, type, provider),
          factor
        ),
      envir = .GlobalEnv
    )
  })

Auswertungen

Gesamtanzahl Fälle und Anfragen

Die Tabelle status enthält zwei Gruppen: Fälle und Anfragen abgeschlossen. In der Spalte category sind auch die Unterkategorien aufgeführt, aber diese beiden sind geeignet, um sich die Summen pro Jahr anzusehen, z.B:

top_four <- c("Salt Mobile SA", "Swisscom (Schweiz) AG",
              "Sunrise Communications AG", "UPC Schweiz GmbH")
status %>%
  filter(category == "Fälle" | category == "Anfragen abgeschlossen") %>%
  group_by(category, year) %>%
  summarise(sum = sum(value)) %>%
  spread(key = category, value = sum) %>%
  mutate(Total = `Anfragen abgeschlossen` + `Fälle`) %>%
  knitr::kable()
year Anfragen abgeschlossen Fälle Total
2014 5036 1184 6220
2015 4849 1420 6269
2016 5115 1333 6448
2017 4183 1125 5308
2018 3330 1078 4408
status %>%
  filter(category == "Fälle" | category == "Anfragen abgeschlossen") %>%
  group_by(category, year, type) %>%
  summarise(sum = sum(value)) %>%
  ggplot(
    aes(
      x = year,
      y = sum,
      fill = type
    )
  ) +
  geom_bar(stat = "identity") +
  scale_fill_brewer(palette = "Set1", guide = FALSE) +
  facet_grid(category ~ type, scales = "fixed") +
  theme_minimal() +
  labs(
    title = "Anzahl Fälle und Anfragen pro Jahr",
    x = NULL,
    y = NULL,
    fill = NULL
  )