{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "+ title: Does Jameda discriminate against non-paying users? Part 2: New Data, new Insights\n", "+ date: 2021-02-20\n", "+ tags: python, plotly, Jameda, premium, doctors\n", "+ Slug: discrimination-on-jameda-analysis-new-data-new-insights\n", "+ Category: Analytics\n", "+ Authors: MC\n", "+ Summary: Does the physicians rating platform Jameda discriminate against non-paying physicians? In a previous post, we analyzed this claim inconclusively. Using new data, we are able to gain new insights and shed light on the issue." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Motivation\n", "\n", "In the [first part]({filename}/jameda_part1.ipynb) of this series, we investigated a claim made in an [article](http://www.zeit.de/2018/04/jameda-aerzte-bewertungsportal-profile-bezahlung/komplettansicht) by the newspaper \"Die Zeit\". The claim was that the popular German physicians rating platform Jameda favors it's paying users while discriminating against non paying physicians. Using a larger and more robust dataset than the original analysis by \"Die Zeit\", we confirmed many of their findings. We found that paying physicians on average have much higher ratings and suspiciously low numbers of poor ratings. However, we took a stance in disagreeing with the conclusion of the article which didn't account for alternative explanations for these observations. As these findings are only based on correlation, we argued that this alone can't be seen as proof for Jameda's favoring of paying members. Especially, as there is a very intuitive theory why paying physicians might have better ratings: Paying to be a premium member on the platform might be related to other positive traits of the physician leading to better ratings. For example, doctors who value their reputation highly might be more careful in interacting with their patients and also more willing to be a paying user. Hence, the result of our first analysis was inconclusive. \n", "However, there still was a credible claim stated by the original article. On Jameda, physicians can report ratings they disagree with. This (temporary) removes the reviews from the site and starts a [validation process](https://www.jameda.de/qualitaetssicherung/). The rating's comment is checked by Jameda and can be removed permanently, if it violates certain rules. It could be that premium members report negative ratings more often. This seems intuitive. Premium members are probably more engaged in cultivating their profiles and also more active in general. In contrast, non paying members might have never looked up their own profile at all. Thus, missing out on the opportunity to report any negative reviews. The author from \"Die Zeit\" asked Jameda whether they remove more negative reviews from premium users' profiles. Jameda stated, that they don't have any data on this. \n", "Well, no worries Jameda. I got your back! I'll gladly offer some data myself to help you out with this question. With a little delay of about three years, we'll finally conclude our analysis. Using new data, we'll be able to shed light on the original question!\n", "
\n", "As always, you can download this notebook on \"Open and \n", " \"Open. Unfortunately, I won't be able to share the underlying data this time. Sorry for that!" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## The data\n", "\n", "The data was scraped once a day and stored to a SQLite database. It is made up of two parts. The first part consists of the information on the physicians. The second part consists of the reviews. (Refer to the [first post]({filename}/jameda_part1.ipynb) for more details on the data content) \n", "We observed changes in the reviews for a period of about nine months from January 2020 to September 2020. Thus, we were able to identify all reviews that were removed by Jameda during this period because they were reported by a physician. Also, we can see the result of the validation process for some of those. They could have been removed after being reported or could have been re published. \n", "First, we create a class for reading the data from our SQLite database:" ] }, { "cell_type": "code", "execution_count": 30, "metadata": { "tags": [ "hide" ] }, "outputs": [ { "data": { "text/html": [ "\n" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "%%HTML\n", "" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "import datetime\n", "import sqlite3\n", "import logging\n", "import pandas as pd\n", "import numpy as np\n", "import plotly.express as px\n", "import plotly.io as pio\n", "from sqlite3 import Error\n", "from pandas.tseries.frequencies import to_offset\n", "from IPython.display import Markdown as md\n", "\n", "pio.renderers.default = \"notebook_connected\"\n", "px.defaults.template = \"plotly_white\"\n", "pd.options.display.max_columns = 100\n", "pd.options.display.max_rows = 600\n", "pd.options.display.max_colwidth = 100\n", "np.set_printoptions(threshold=2000)\n", "log = logging.getLogger()\n", "log.setLevel(logging.DEBUG)\n", "\n", "# Data for original observation period\n", "DATA_OLD = \"../data/raw/2020-09-23_jameda.db\"\n", "# To check for longer term changes we got some new data\n", "DATA_NEW = \"../data/raw/2021-02-13_jameda.db\"\n", "DATE_START_WAVE2 = \"2021-02-10\"" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [], "source": [ "class DB:\n", " def __init__(self, db):\n", " \"\"\"\n", " Connect to sqllite DB expose connection and cursor of instance\n", " \"\"\"\n", " self.cursor = None\n", " self.conn = self.create_connection(db)\n", " self.conn.row_factory = sqlite3.Row # return column names on fetch\n", " try:\n", " self.cursor = self.conn.cursor()\n", " except Exception as e:\n", " log.exception(f\"Error getting cursor for DB connection: {e}\")\n", "\n", " def create_connection(self, db_path):\n", " \"\"\"return a database connection \"\"\"\n", " try:\n", " conn = sqlite3.connect(db_path)\n", " except Error:\n", " log.exception(f\"Error connecting to DB: {db_path}\")\n", " return conn\n", "\n", " def send_single_statement(self, statement):\n", " \"\"\" Send single statement to DB \"\"\"\n", " try:\n", " self.cursor.execute(statement)\n", " except Error:\n", " log.exception(f\"Error sending statement: {statement}\")\n", " self.conn.rollback()\n", " return None\n", " else:\n", " log.info(f\"OK sending statement: {statement}\")\n", " self.conn.commit()\n", " return True\n", "\n", " def select_and_fetchall(self, statement):\n", " \"\"\" Execute a select statement and return all rows \"\"\"\n", " try:\n", " self.cursor.execute(statement)\n", " rows = self.cursor.fetchall()\n", " except Exception:\n", " log.exception(\"Could not select and fetchall\")\n", " return None\n", " else:\n", " return rows\n", "\n", " def __del__(self):\n", " \"\"\" make sure we close connection \"\"\"\n", " self.conn.close()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Using the class and its methods, let's read in the data regarding the doctors' profiles:" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
ref_idstrasseanredegesamt_notebewertungenorttypplzscoreartentfernunglatlngname_nicename_kurztyp_stringfach_stringurlurl_hintensnippetscanHaveReviewsportraitdate_createdate_updateerrorspremium
080283417Venloer Str. 38922.438KölnHA5082551.263910.150.9510996.915398Dr. med. Brigitte JähnigDr. JähnigÄrztinKinderärztin/koeln/aerzte/kinderaerzte/dr-brigitte-jaehnig/80283417_1/[{'type': 'positive', 'label': 'öffentlich gut erreichbar'}, {'type': 'positive', 'label': 'freu...1None2020-01-07 18:16:352020-01-07 18:16:3500
180424197Venloer Str. 38921.001KölnHA5082553.663710.150.9510996.915398Dr. med. Andrea SteinleDr. SteinleÄrztinInternistin/koeln/aerzte/innere-allgemeinmediziner/dr-andrea-steinle/80424197_1/None1None2020-01-07 18:16:352020-01-07 18:16:3500
\n", "
" ], "text/plain": [ " ref_id strasse anrede gesamt_note bewertungen ort typ \\\n", "0 80283417 Venloer Str. 389 2 2.43 8 Köln HA \n", "1 80424197 Venloer Str. 389 2 1.00 1 Köln HA \n", "\n", " plz score art entfernung lat lng \\\n", "0 50825 51.2639 1 0.1 50.951099 6.915398 \n", "1 50825 53.6637 1 0.1 50.951099 6.915398 \n", "\n", " name_nice name_kurz typ_string fach_string \\\n", "0 Dr. med. Brigitte Jähnig Dr. Jähnig Ärztin Kinderärztin \n", "1 Dr. med. Andrea Steinle Dr. Steinle Ärztin Internistin \n", "\n", " url url_hinten \\\n", "0 /koeln/aerzte/kinderaerzte/dr-brigitte-jaehnig/ 80283417_1/ \n", "1 /koeln/aerzte/innere-allgemeinmediziner/dr-andrea-steinle/ 80424197_1/ \n", "\n", " snippets \\\n", "0 [{'type': 'positive', 'label': 'öffentlich gut erreichbar'}, {'type': 'positive', 'label': 'freu... \n", "1 None \n", "\n", " canHaveReviews portrait date_create date_update errors \\\n", "0 1 None 2020-01-07 18:16:35 2020-01-07 18:16:35 0 \n", "1 1 None 2020-01-07 18:16:35 2020-01-07 18:16:35 0 \n", "\n", " premium \n", "0 0 \n", "1 0 " ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# from sqlite to df\n", "db = DB(DATA_OLD)\n", "ret = db.select_and_fetchall(\"SELECT * FROM doctors;\")\n", "rows = [dict(row) for row in ret]\n", "docs = pd.DataFrame(rows)\n", "docs[\"premium\"] = 0\n", "# If user has a portrait picture, he is paying member / premium user\n", "docs.loc[docs[\"portrait\"].notna(), \"premium\"] = 1\n", "docs_unq = (\n", " docs.sort_values([\"ref_id\", \"date_update\"])\n", " .groupby(\"ref_id\", as_index=False)\n", " .agg(\"last\")\n", ")\n", "# Clean doctor subject string\n", "docs[\"fach_string\"] = docs[\"fach_string\"].str.replace(\"[\\['\\]]\", \"\")\n", "docs.head(2)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This is very similar to the data we used in the first post. In this analysis, most of the columns can be ignored. We'll focus on the `ref_id` (which is the user id) and whether or not the physician is a paying (`premium = 1`) or non-paying user (`premium = 0`). \n", "Next, we read in and process the reviews:" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [], "source": [ "# from multiple sqlite DBs to single df\n", "DATA = [DATA_OLD, DATA_NEW]\n", "reviews = pd.DataFrame()\n", "for data in DATA:\n", " db = DB(data)\n", " ret = db.select_and_fetchall(\"SELECT * FROM reviews;\")\n", " rows = [dict(row) for row in ret]\n", " df = pd.DataFrame(rows).sort_values([\"ref_id\", \"b_id\", \"date_create\"])\n", " # columns to dates\n", " df[\"b_date\"] = pd.to_datetime(df[\"b_date\"], unit=\"s\")\n", " df[\"date_create\"] = pd.to_datetime(df[\"date_create\"])\n", " df[\"date_update\"] = pd.to_datetime(df[\"date_update\"])\n", " reviews = reviews.append(df)" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [], "source": [ "# some processing\n", "# force numeric content to numeric type\n", "reviews[\n", " [\n", " \"ref_id\",\n", " \"b_id\",\n", " \"b_stand\",\n", " \"gesamt_note_class\",\n", " \"br_total_votes\",\n", " \"br_index\",\n", " \"is_archived\",\n", " \"kommentar_entfernt\",\n", " ]\n", "] = reviews[\n", " [\n", " \"ref_id\",\n", " \"b_id\",\n", " \"b_stand\",\n", " \"gesamt_note_class\",\n", " \"br_total_votes\",\n", " \"br_index\",\n", " \"is_archived\",\n", " \"kommentar_entfernt\",\n", " ]\n", "].apply(\n", " pd.to_numeric\n", ")\n", "# flag wave 1 and 2\n", "reviews[\"wave\"] = np.where(reviews[\"date_create\"] < DATE_START_WAVE2, 1, 2)\n", "reviews_w2 = reviews[reviews[\"wave\"] == 2]\n", "reviews_w1 = reviews[reviews[\"wave\"] == 1]\n", "# skip incomplete days\n", "date_firstday = reviews_w1[\"date_create\"].min().ceil(\"d\")\n", "date_lastday = reviews_w1[\"date_create\"].max().floor(\"d\")\n", "reviews_w1 = reviews_w1.loc[\n", " (reviews_w1[\"date_create\"] >= date_firstday)\n", " & (reviews_w1[\"date_create\"] <= date_lastday),\n", "]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The `ref_id` of each review can be related back to the corresponding physician. The `b_id` refers to the unique id of a review. Moreover, we'll need `gesamt_note` which is the numerical rating of the review (from 1 = best, to 6 = worst), `b_date` which is the first publication date of a review, `date_create` which is the date we observed this review and `b_stand` which is the status of the review (explained below). \n", "Here, we strictly look at reviews for which we have multiple observations, i.e. reviews which have changed during the period we scraped them:" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [], "source": [ "# filter for reviews with multiple entries (new entry only created when reviews changed)\n", "num_entries = reviews_w1.groupby(\"b_id\").size()\n", "multi_entry = reviews_w1.loc[\n", " reviews_w1[\"b_id\"].isin(num_entries[num_entries > 1].index),\n", "].sort_values([\"ref_id\", \"b_id\", \"date_create\"])\n", "\n", "# filter on relevant columns\n", "cols_change = [\"ref_id\", \"b_date\", \"b_id\", \"b_stand\", \"date_create\", \"wave\"]\n", "multi_entry = multi_entry[cols_change].reset_index(drop=True)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now, we come back to the `b_stand` (status) variable. We know that `1` means the status is normal. This is just a regular review, containing a rating and a comment. When `b_stand` is `4` it indicates that the review was reported, temporarily removed, and is being verified by Jameda (the process is explained [here](https://www.jameda.de/qualitaetssicherung/)). A `5` tells us, that the review has a comment but no rating. Here's an example for a reported review:" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "titel Warum ist diese Bewertung aktuell nicht online?\n", "kommentar Dr. Herrmann hat uns die Bewertung gemeldet, da sie sie für rechtswidrig hält. Aus diesem G...\n", "Name: 55045, dtype: object\n" ] } ], "source": [ "print(\n", " reviews_w1.loc[reviews_w1[\"b_stand\"] == 4,].iloc[\n", " 0\n", " ][[\"titel\", \"kommentar\"]]\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "These reviews are grayed out on the website. The rating is deleted and the original text in the title and comment is replaced by a standard message. It says something along the lines of: \"This review has been reported by the physician and is under review\". Also, the rating is not displayed for them anymore. \n", "Following, for the reviews with multiple entries, we check how `b_stand` changed between observations:" ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
ref_idb_dateb_idb_standdate_createwaveb_stand_prevchangeremovedreadded
0800009392016-07-14 18:17:01281701512020-07-15 03:22:201NaNNaNNaNNaN
1800009392016-07-14 18:17:01281701512020-08-23 03:03:1811.00.0NaNNaN
2800009412016-06-10 12:49:17275413412020-02-25 03:24:121NaNNaNNaNNaN
3800009412016-06-10 12:49:17275413412020-09-19 03:02:1911.00.0NaNNaN
\n", "
" ], "text/plain": [ " ref_id b_date b_id b_stand date_create wave \\\n", "0 80000939 2016-07-14 18:17:01 2817015 1 2020-07-15 03:22:20 1 \n", "1 80000939 2016-07-14 18:17:01 2817015 1 2020-08-23 03:03:18 1 \n", "2 80000941 2016-06-10 12:49:17 2754134 1 2020-02-25 03:24:12 1 \n", "3 80000941 2016-06-10 12:49:17 2754134 1 2020-09-19 03:02:19 1 \n", "\n", " b_stand_prev change removed readded \n", "0 NaN NaN NaN NaN \n", "1 1.0 0.0 NaN NaN \n", "2 NaN NaN NaN NaN \n", "3 1.0 0.0 NaN NaN " ] }, "execution_count": 9, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Compute direction of b_stand change: review removed or re added (after removal)\n", "multi_entry = multi_entry[multi_entry[\"b_stand\"].isin([1, 4, 5])]\n", "multi_entry[\"b_stand_prev\"] = multi_entry.groupby(\"b_id\")[\"b_stand\"].shift(\n", " fill_value=np.nan\n", ")\n", "multi_entry[\"change\"] = multi_entry[\"b_stand\"] - multi_entry[\"b_stand_prev\"]\n", "multi_entry.loc[multi_entry[\"change\"].isin([3, -1]), \"removed\"] = 1\n", "multi_entry.loc[multi_entry[\"change\"].isin([-3, 1]), \"readded\"] = 1\n", "multi_entry.head(4)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Using the change in `b_stand` we can conclude whether a review has been removed (it went from 1 to 4 or 5 to 4) or re added after being removed some time before (change from 4 to 1 or 4 to 5). \n", "Let's store a data frame of only the reviews which have been removed or re added:" ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
b_idremovedreadded
06828471.0NaN
18070421.0NaN
\n", "
" ], "text/plain": [ " b_id removed readded\n", "0 682847 1.0 NaN\n", "1 807042 1.0 NaN" ] }, "execution_count": 10, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Only reviews that were added or removed\n", "changed = multi_entry[(multi_entry[\"removed\"] == 1) | (multi_entry[\"readded\"] == 1)]\n", "changed = changed[[\"b_id\", \"removed\", \"readded\"]].groupby(\"b_id\").max().reset_index()\n", "changed.head(2)" ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
ref_idb_idb_standu_alterkasse_privatb_dategesamt_notegesamt_note_class_xgesamt_note_formattedbs_inhaltbr_total_votesbr_total_valuebr_indexis_archivedtitelkommentar_entferntkommentarheaderfragendate_createdate_updatewaveremovedreaddedpremiumortfach_stringurlurl_hintengesamt_note_class_y
08000072733978801002017-05-23 09:48:031.001.01,002.063.00Sehr gute Kompetenz mit perfekten Netzwerk0Hier fühlt man sich bestens aufgehoben und wird perfekt betreut in einen sehr schönen Ambiente.Bewertung vom 23.05.17[{'fragekurz': 'Behandlung', 'note': '1'}, {'fragekurz': 'Aufklärung', 'note': '1'}, {'fragekurz...2020-09-09 03:24:192020-09-09 03:24:1910.00.01MünchenInternistin/muenchen/aerzte/innere-allgemeinmediziner/dr-daniela-grenacher-horn/80000727_1/1.0
18000072734135801212017-06-01 19:11:211.001.01,003.031.00Eine super Ärztin mit viel Leidenschaft, Menschlichkeit und Beruf als Berufung0Durch ganz großes Glück kam Ich zu Frau Dr. Grenacher-Horn. <br />\\r\\n<br />\\r\\nIch habe noch ni...Bewertung vom 01.06.17, gesetzlich versichert, 30 bis 50[{'fragekurz': 'Behandlung', 'note': '1'}, {'fragekurz': 'Aufklärung', 'note': '1'}, {'fragekurz...2020-09-09 03:24:192020-09-09 03:24:1910.00.01MünchenInternistin/muenchen/aerzte/innere-allgemeinmediziner/dr-daniela-grenacher-horn/80000727_1/1.0
\n", "
" ], "text/plain": [ " ref_id b_id b_stand u_alter kasse_privat b_date \\\n", "0 80000727 3397880 1 0 0 2017-05-23 09:48:03 \n", "1 80000727 3413580 1 2 1 2017-06-01 19:11:21 \n", "\n", " gesamt_note gesamt_note_class_x gesamt_note_formatted bs_inhalt \\\n", "0 1.00 1.0 1,0 0 \n", "1 1.00 1.0 1,0 0 \n", "\n", " br_total_votes br_total_value br_index is_archived \\\n", "0 2.0 6 3.0 0 \n", "1 3.0 3 1.0 0 \n", "\n", " titel \\\n", "0 Sehr gute Kompetenz mit perfekten Netzwerk \n", "1 Eine super Ärztin mit viel Leidenschaft, Menschlichkeit und Beruf als Berufung \n", "\n", " kommentar_entfernt \\\n", "0 0 \n", "1 0 \n", "\n", " kommentar \\\n", "0 Hier fühlt man sich bestens aufgehoben und wird perfekt betreut in einen sehr schönen Ambiente. \n", "1 Durch ganz großes Glück kam Ich zu Frau Dr. Grenacher-Horn.
\\r\\n
\\r\\nIch habe noch ni... \n", "\n", " header \\\n", "0 Bewertung vom 23.05.17 \n", "1 Bewertung vom 01.06.17, gesetzlich versichert, 30 bis 50 \n", "\n", " fragen \\\n", "0 [{'fragekurz': 'Behandlung', 'note': '1'}, {'fragekurz': 'Aufklärung', 'note': '1'}, {'fragekurz... \n", "1 [{'fragekurz': 'Behandlung', 'note': '1'}, {'fragekurz': 'Aufklärung', 'note': '1'}, {'fragekurz... \n", "\n", " date_create date_update wave removed readded premium \\\n", "0 2020-09-09 03:24:19 2020-09-09 03:24:19 1 0.0 0.0 1 \n", "1 2020-09-09 03:24:19 2020-09-09 03:24:19 1 0.0 0.0 1 \n", "\n", " ort fach_string \\\n", "0 München Internistin \n", "1 München Internistin \n", "\n", " url \\\n", "0 /muenchen/aerzte/innere-allgemeinmediziner/dr-daniela-grenacher-horn/ \n", "1 /muenchen/aerzte/innere-allgemeinmediziner/dr-daniela-grenacher-horn/ \n", "\n", " url_hinten gesamt_note_class_y \n", "0 80000727_1/ 1.0 \n", "1 80000727_1/ 1.0 " ] }, "execution_count": 11, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# keep only unique reviews and add cols from other tables with infos\n", "reviews_unq = reviews_w1[reviews_w1[\"b_stand\"].isin([1, 4, 5])]\n", "reviews_unq = reviews_unq.drop_duplicates(\"b_id\", keep=\"last\")\n", "reviews_unq = reviews_unq.merge(changed, how=\"left\", on=\"b_id\")\n", "doc_infos = docs[\n", " [\"ref_id\", \"premium\", \"ort\", \"fach_string\", \"url\", \"url_hinten\"]\n", "].drop_duplicates(\"ref_id\", keep=\"last\")\n", "reviews_unq = reviews_unq.merge(doc_infos, how=\"left\", on=\"ref_id\")\n", "# store original rating (on removal of review grade disappears) and re add to reviews\n", "ratings = reviews_w1[[\"gesamt_note_class\", \"b_id\"]].groupby(\"b_id\").min().reset_index()\n", "reviews_unq = reviews_unq.merge(ratings, how=\"left\", on=\"b_id\")\n", "reviews_unq = reviews_unq.replace(np.nan, 0)\n", "# remove those without a grade (under review without previous entry)\n", "# reviews_unq = reviews_unq[reviews_unq[\"gesamt_note_class_y\"] > 0]\n", "reviews_unq.head(2)" ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
b_idb_datedate_createremovedreaddedpremiumgesamt_note_class_ywave
046829672019-07-12 09:34:262020-03-02 03:21:151.00.016.01
148716982019-12-02 10:37:172020-03-02 03:21:151.00.016.01
\n", "
" ], "text/plain": [ " b_id b_date date_create removed readded premium \\\n", "0 4682967 2019-07-12 09:34:26 2020-03-02 03:21:15 1.0 0.0 1 \n", "1 4871698 2019-12-02 10:37:17 2020-03-02 03:21:15 1.0 0.0 1 \n", "\n", " gesamt_note_class_y wave \n", "0 6.0 1 \n", "1 6.0 1 " ] }, "execution_count": 12, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Only reviews that were removed With additional cols\n", "removed = reviews_unq.loc[\n", " (reviews_unq[\"removed\"] == 1),\n", " [\n", " \"b_id\",\n", " \"b_date\",\n", " \"date_create\",\n", " \"removed\",\n", " \"readded\",\n", " \"premium\",\n", " \"gesamt_note_class_y\",\n", " \"wave\",\n", " ],\n", "].reset_index(drop=True)\n", "removed.head(2)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Analysis\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "After we have cleaned and prepared the data, we can check some of its properties. First, let's see how many new reviews are published each week during our observation period:" ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [ { "data": { "text/html": [ " \n", " " ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/html": [ "
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "# Only reviews that were published during observation wave 1\n", "reviews_unq_new_in_wave1 = reviews_unq.loc[\n", " (reviews_unq[\"b_date\"] >= date_firstday) & (reviews_unq[\"b_date\"] <= date_lastday),\n", "]\n", "\n", "# From daily data to weekly aggregation\n", "plt = reviews_unq_new_in_wave1[[\"b_date\", \"premium\"]].set_index(\"b_date\")\n", "plt[\"reviews\"] = 1\n", "plt[\"non premium\"] = abs(plt[\"premium\"] - 1)\n", "plt = plt.resample(\"W-MON\", label=\"left\").sum()\n", "reviews_new_total = plt[\"reviews\"].sum()\n", "\n", "fig = px.bar(\n", " plt,\n", " x=plt.index,\n", " y=[\"non premium\", \"premium\"],\n", " title=f\"New reviews per week in 2020 (Total {reviews_new_total})\",\n", " labels={\"b_date\": \"Date published\", \"variable\": \"Published reviews\"},\n", " barmode=\"stack\",\n", ")\n", "fig.update_xaxes(dtick=7 * 24 * 60 * 60 * 1000, tickformat=\"%d %b\", tick0=\"2020-01-06\")\n", "fig.show()" ] }, { "cell_type": "code", "execution_count": 14, "metadata": { "tags": [ "hide" ] }, "outputs": [ { "data": { "text/markdown": [ "Each week, there are between 191 and 862 newly published reviews. The average week sees about 617 of them. Reviews on premium profiles have a share of 42%." ], "text/plain": [ "" ] }, "execution_count": 14, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Share of Premium\n", "reviews_new_total_prem = plt[\"premium\"].sum()\n", "reviews_new_total_noprem = plt[\"non premium\"].sum()\n", "reviews_new_share_prem = (\n", " reviews_new_total_prem / (reviews_new_total_prem + reviews_new_total_noprem) * 100\n", ")\n", "# descriptives\n", "reviews_min = plt[\"reviews\"].min()\n", "reviews_max = plt[\"reviews\"].max()\n", "reviews_mean = plt[\"reviews\"].mean()\n", "md(\n", " f\"Each week, there are between {reviews_min} and {reviews_max} newly published reviews. The average week sees about {reviews_mean:.0f} of them. Reviews on premium profiles have a share of {reviews_new_share_prem:.0f}%.\"\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Next, we compare those numbers to the number of reviews removed during the same time span:" ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Removed reviews: 459 (0.18% of all, 2.01% of new reviews in period)\n" ] } ], "source": [ "freq_removed = reviews_unq[\"removed\"].value_counts()\n", "freq_removed_perc = reviews_unq[\"removed\"].value_counts(normalize=True) * 100\n", "freq_removed_perc_of_new = freq_removed / reviews_new_total * 100\n", "print(\n", " f\"Removed reviews: {freq_removed[1]:.0f} ({freq_removed_perc[1]:.2f}% of all\"\\\n", " f\", {freq_removed_perc_of_new[1]:.2f}% of new reviews in period)\"\n", ")" ] }, { "cell_type": "code", "execution_count": 16, "metadata": { "tags": [ "hide" ] }, "outputs": [ { "data": { "text/markdown": [ "Over the nine month period in which we observed all changes in the reviews on a daily basis, we find only 459 removed reviews. That amounts to only 0.18% of all reviews and 2.01% of the newly published reviews during that same observation period." ], "text/plain": [ "" ] }, "execution_count": 16, "metadata": {}, "output_type": "execute_result" } ], "source": [ "md(\n", " f\"Over the nine month period in which we observed all changes in the reviews on a daily basis, we find only {freq_removed[1]:.0f} removed reviews. That amounts to only {freq_removed_perc[1]:.2f}% of all reviews and {freq_removed_perc_of_new[1]:.2f}% of the newly published reviews during that same observation period.\"\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In general, **the removal of reviews seems not to be very common**. Still, **it could have a substantial impact on total ratings**. As there are only few negative reviews in general, removing those can alter the picture greatly. \n", "As before, we visualize the removed reviews by week and check for patterns:" ] }, { "cell_type": "code", "execution_count": 17, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "# From daily data to weekly aggregation\n", "plt = removed[[\"date_create\", \"removed\", \"premium\"]].set_index(\"date_create\")\n", "plt = plt.resample(\"W-MON\", label=\"left\").sum()\n", "plt[\"non premium\"] = plt[\"removed\"] - plt[\"premium\"]\n", "reviews_removed_total = plt[\"removed\"].sum()\n", "\n", "fig = px.bar(\n", " plt,\n", " x=plt.index,\n", " y=[\"non premium\", \"premium\"],\n", " title=f\"Removed reviews per week in 2020 (Total {reviews_removed_total:.0f})\",\n", " labels={\"date_create\": \"Date\", \"variable\": \"Removed Reviews\"},\n", " barmode=\"stack\",\n", ")\n", "fig.update_xaxes(dtick=7 * 24 * 60 * 60 * 1000, tickformat=\"%d %b\", tick0=\"2020-01-06\")\n", "fig.show()" ] }, { "cell_type": "code", "execution_count": 18, "metadata": {}, "outputs": [], "source": [ "# Share of Premium\n", "reviews_removed_prem = plt[\"premium\"].sum()\n", "reviews_removed_prem_share = reviews_removed_prem / reviews_removed_total * 100\n", "\n", "# descriptives\n", "reviews_min = int(plt[\"removed\"].min())\n", "reviews_max = int(plt[\"removed\"].max())\n", "reviews_mean = plt[\"removed\"].mean()" ] }, { "cell_type": "code", "execution_count": 19, "metadata": { "tags": [ "hide" ] }, "outputs": [ { "data": { "text/markdown": [ "Each week, between 1 and 38 reviews are removed. In an average week there are about 12 of them. Out of all removed reviews during the observation period, those on premium profiles have a share of 24%. Hence, **the share of removed reviews is substantially lower then the share of published reviews (42%, see above) on premium profiles.**" ], "text/plain": [ "" ] }, "execution_count": 19, "metadata": {}, "output_type": "execute_result" } ], "source": [ "md(\n", " f\"Each week, between {reviews_min} and {reviews_max} reviews are removed. In an average week there are about {reviews_mean:.0f} of them. Out of all removed reviews during the observation period, those on premium profiles have a share of {reviews_removed_prem_share:.0f}%. Hence, **the share of removed reviews is substantially lower then the share of published reviews ({reviews_new_share_prem:.0f}%, see above) on premium profiles.**\"\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "So, there is no problem with reviews being disproportionately removed from premium profiles to improve their ratings? Not so fast! While this seems to hold true, it's not the right question to ask. We've learned before, that in general premium users get way less poor ratings than non paying users (see [first part]({filename}/jameda_part1.ipynb)). Also, it's intuitive that removed reviews will predominantly have low ratings (we'll check that in a minute). Consequently, the real question is: **\"Are relatively more critical reviews (i.e. those with low ratings) removed from premium profiles?\"**. Thus, we also need to take the ratings into account when comparing groups:" ] }, { "cell_type": "code", "execution_count": 20, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "premium removed\n", "0 0.0 98.880847\n", " 1.0 1.119153\n", "1 0.0 96.618357\n", " 1.0 3.381643\n", "Name: removed, dtype: float64\n" ] } ], "source": [ "# Filter for poor reviews\n", "reviews_poor = reviews_unq[reviews_unq[\"gesamt_note_class_y\"] >= 4]\n", "# How many of the poor ratings are removed by status\n", "share = reviews_poor.groupby([\"premium\"])[\"removed\"].value_counts(normalize=True) * 100\n", "prob_removed_premium = share[1][1] / share[0][1]\n", "print(share)" ] }, { "cell_type": "code", "execution_count": 21, "metadata": { "tags": [ "hide" ] }, "outputs": [ { "data": { "text/markdown": [ "The answer to the above is: **Yes**. On premium profiles 3.4% of poor reviews are removed but only 1.1% are removed on non premium profiles. **A poor rating on a premium profile is 3 times more likely to be removed compared to one on a non premium profile**." ], "text/plain": [ "" ] }, "execution_count": 21, "metadata": {}, "output_type": "execute_result" } ], "source": [ "md(\n", " f\"The answer to the above is: **Yes**. On premium profiles {share[1][1]:.1f}% of poor reviews are removed but only {share[0][1]:.1f}% are removed on non premium profiles. **A poor rating on a premium profile is {prob_removed_premium:.0f} times more likely to be removed compared to one on a non premium profile**.\"\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In the following, we look a bit closer at the reviews that get removed. As stated above, we'd expect them to have strictly negative ratings:" ] }, { "cell_type": "code", "execution_count": 22, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "# frequency removed by rating\n", "plt = removed[[\"gesamt_note_class_y\", \"removed\", \"premium\"]].reset_index(drop=True)\n", "plt = (\n", " plt.value_counts(\"gesamt_note_class_y\", normalize=True)\n", " .rename(\"frequency\")\n", " .reset_index()\n", ")\n", "\n", "fig = px.bar(\n", " plt,\n", " x=\"gesamt_note_class_y\",\n", " y=\"frequency\",\n", " title=\"Rating frequency of removed reviews\",\n", " labels={\"gesamt_note_class_y\": \"Rating\"},\n", ")\n", "fig.update_yaxes(tickformat=\"%\")\n", "fig.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Well, our intuition was pretty much right. **By far, most of the removed reviews have a poor rating**. Still, a few of the removed reviews had a good rating. Turns out, those are mostly misrated cases where a positive rating was given to a critical comment. (Notice: A rating of 0 means that the review didn't have a rating at all, i.e. it was just a comment)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Next, we ask ourselves: How long is the typical time span between a critical rating being published and it being reported (more precisely removed, as we don't now how much time passes from report to removal)? We look at all reviews that were removed during the observation period and compare the time of removal to the time of creation (which can be well before our observation phase):" ] }, { "cell_type": "code", "execution_count": 23, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
b_idb_datedate_createremovedreaddedpremiumgesamt_note_class_ywavereport_durationreport_duration_days
046829672019-07-12 09:34:262020-03-02 03:21:151.00.016.01233 days 17:46:49233
148716982019-12-02 10:37:172020-03-02 03:21:151.00.016.0190 days 16:43:5890
\n", "
" ], "text/plain": [ " b_id b_date date_create removed readded premium \\\n", "0 4682967 2019-07-12 09:34:26 2020-03-02 03:21:15 1.0 0.0 1 \n", "1 4871698 2019-12-02 10:37:17 2020-03-02 03:21:15 1.0 0.0 1 \n", "\n", " gesamt_note_class_y wave report_duration report_duration_days \n", "0 6.0 1 233 days 17:46:49 233 \n", "1 6.0 1 90 days 16:43:58 90 " ] }, "execution_count": 23, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# compute duration between review published and removed in days\n", "removed[\"report_duration\"] = removed[\"date_create\"] - removed[\"b_date\"]\n", "removed[\"report_duration_days\"] = removed[\"report_duration\"].map(lambda x: x.days)\n", "removed.head(2)" ] }, { "cell_type": "code", "execution_count": 24, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "# Visualize time delta from publishing to removal for removed reviews\n", "plt = removed[[\"report_duration_days\", \"removed\"]].reset_index(drop=True)\n", "\n", "fig = px.histogram(\n", " plt,\n", " x=\"report_duration_days\",\n", " title=\"Removed reviews: days between publishing and removal\",\n", " labels={\"report_duration_days\": \"Days\"},\n", " histnorm=\"probability\",\n", " nbins=200,\n", " marginal=\"box\",\n", ")\n", "fig.update_yaxes(tickformat=\"%\")\n", "fig.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This chart give us some nice insights about the whole reporting / removal process: \n", "First, the record for the most short lived comment is only four days. That might be a good proxy for the minimum reaction time of Jameda, i.e. the time between receiving a report and acting on it. About 12% of the removed reviews are removed within 20 days. But in general, **it takes about three months for a review to be removed**. Nonetheless, there are quite a few reviews that get removed much later. In one case, the review was removed after more than seven years! \n", "If we compare this distribution between paying and non paying physicians, we might learn some more:" ] }, { "cell_type": "code", "execution_count": 25, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "# Visualize time delta from publishing to removal for removed reviews\n", "plt = removed[[\"report_duration_days\", \"removed\", \"premium\"]].reset_index(drop=True)\n", "plt = plt[(plt[\"removed\"] == 1)]\n", "\n", "fig = px.histogram(\n", " plt,\n", " x=\"report_duration_days\",\n", " color=\"premium\",\n", " title=\"Removed reviews: days between publishing and removal\",\n", " labels={\"report_duration_days\": \"Days\"},\n", " histnorm=\"probability\",\n", " nbins=200,\n", " marginal=\"box\",\n", ")\n", "fig.update_yaxes(tickformat=\"%\")\n", "fig.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Those distributions look quite different and support our previous hypothesis: Premium users seem in fact to be more concerned about their reputation on Jameda. **Critical reviews on their profiles are removed (reported) much faster**. On median, this is the case after 52 days while for non premium users the median is 139 days." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As a last analysis, let's see what happens to the removed reviews we observed after some time has passed. Following our original observation period in 2020 (wave 1), we updated our review data in February 2021 (wave 2). After about five months since the end of wave 1, how many of the removed reviews have been re published? How many have been deleted for good? " ] }, { "cell_type": "code", "execution_count": 26, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Removed during wave one: 459\n", "Out of those, re added until wave two: 33\n", "Percent re added of removed total: 7.2%\n", "Percent re added of removed for non premium: 7.8%\n", "Percent re added of removed for premium: 5.4%\n" ] } ], "source": [ "# Which removed during wave 1 are re added until wave 2\n", "print(\"Removed during wave one:\", removed[\"b_id\"].shape[0])\n", "readded = removed.merge(reviews_w2, on=\"b_id\")\n", "readded = readded[readded[\"b_stand\"].isin([1, 5])]\n", "print(\"Out of those, re added until wave two:\", readded.shape[0])\n", "pct_readded = readded.shape[0] / removed[\"b_id\"].shape[0] * 100\n", "print(f\"Percent re added of removed total: {pct_readded:.1f}%\")\n", "# Perct of removed is re added by status\n", "pct_readded_by_status = (\n", " readded[\"premium\"].value_counts() / removed[\"premium\"].value_counts() * 100\n", ")\n", "print(f\"Percent re added of removed for non premium: {pct_readded_by_status[0]:.1f}%\")\n", "print(f\"Percent re added of removed for premium: {pct_readded_by_status[1]:.1f}%\")" ] }, { "cell_type": "code", "execution_count": 27, "metadata": { "jupyter": { "source_hidden": true }, "tags": [ "hide" ] }, "outputs": [ { "data": { "text/markdown": [ "**When a review gets removed, it stays removed in most cases**. Only 7.2% of all removed reviews are re published after five months or longer in our observation. This share differs by status. For reviews on non premium profiles the share of re added reviews is 7.8% but for premium profiles it is only 5.4%. This could indicate that Jameda does not validate all reported reviews equally and therefor favors premium users. However, the number of observations is low and there are a few necessary assumptions that can't be checked (e.g. that the share of reported reviews violating the rules is identical). Hence, this is speculative and can't be backed by the data.
Nonetheless, **reporting unpleasant reviews seems like a good strategy for physicians in order to improve their ratings.**" ], "text/plain": [ "" ] }, "execution_count": 27, "metadata": {}, "output_type": "execute_result" } ], "source": [ "md(\n", " f\"**When a review gets removed, it stays removed in most cases**. Only {pct_readded:.1f}% of all removed reviews are re published after five months or longer in our observation. This share differs by status. For reviews on non premium profiles the share of re added reviews is {pct_readded_by_status[0]:.1f}% but for premium profiles it is only {pct_readded_by_status[1]:.1f}%. This could indicate that Jameda does not validate all reported reviews equally and therefor favors premium users. However, the number of observations is low and there are a few necessary assumptions that can't be checked (e.g. that the share of reported reviews violating the rules is identical). Hence, this is speculative and can't be backed by the data.
Nonetheless, **reporting unpleasant reviews seems like a good strategy for physicians in order to improve their ratings.**\"\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Conclusion\n", "\n", "This post was a sequel to the [original analysis]({filename}/jameda_part1.ipynb) which investigated whether Jameda favors its paying users. By collecting and analyzing new data over a period of about nine months, we were able to overcome the limitations of the first analysis. In particular, we focused on insights referring to the removal of critical reviews on the profiles of physicians. The main takeaways are these:\n", "\n", "1. Jameda has a lot of active users. Each week a lot of reviews are created by patients and published\n", "2. About 42% of the created reviews are published on the profiles of premium physicians\n", "3. Critical reviews can be reported by physicians and are removed until Jameda has validated them. The share of removed reviews on premium profiles is only 23% of the total removed reviews\n", "4. Removed reviews overwhelmingly have poor ratings\n", "5. Removed reviews are seldom. However, they exclusively target poor ratings which are rare. As such, the removal can significantly alter profiles\n", "6. On premium profiles, reviews with poor ratings are three times more likely to be removed compared to those on non premium profiles\n", "7. Critical reviews on premium profiles are removed much faster than those on non premium profiles\n", "8. Once deleted, a rating is very unlikely to ever be re published. Only 7.2% of removed reviews were re published after >5 months\n", "9. There is some dubious hint, that removed reviews on premium profiles are more likely to stay removed\n", "\n", "The last few points are the most relevant ones for answering our original question. **Differences in the total ratings of physicians on Jameda are not solely due to received ratings**. The removal of reviews plays a role as well: Poor reviews are removed faster and more often from premium users' profiles. This is particularly impactful, because a deleted reviews is very unlikely to ever be re published again. **As a result, at least some profiles will have inflated ratings (on average, those are premium profiles)**. This has serious consequences. Not only is the rating a strong signal for potential patients but Jameda also uses it as a default sorting criterion in the search. Consequently, physicians with higher ratings will get more patients through the platform. \n", "While this might seem unfair, it's not easy to assign guilt. It's likely a consequence of the greater effort that premium users put in maintaining their good reputation on the platform. They are simply more inclined to report critical reviews. Also, it is not too far-fetched to believe that there are good reasons that at least some of the reviews are removed. While this is not favoring premium users, it certainly is a disadvantage for physicians that are not actively monitoring their profiles. Those are usually the non paying users. \n", "Jameda's main responsibility is to ensure that the reporting of reviews is not abused. This includes making sure that reviews are validated fast and re published if they don't violate any rules. It's questionable if this is currently the case. Only a small share of removed reviews seems to ever be re added. \n", "However, one must also give Jameda some credit. It's a very complicated matter of law to decide which reviews are permissible. Erring on the side of removal might be the safest strategy for them. Also, there have been some efforts to penalize abusive physicians (i.e. those that report reviews on baseless grounds). They can get their [quality seal retracted](https://www.jameda.de/qualitaetssicherung/bewertung-loeschen/). \n", "The most critical aspect is that Jameda must treat all reported reviews equally. If they don't, that would really be a severe case of misconduct. Unfortunately, it might be next to impossible to tell \"from the outside\". \n", "To sum up: \n", "it would probably be too simple to judge Jameda very harshly for the outcome. Nonetheless, it's clear that the outcome is sub optimal for at least some (potential) patients and physicians: **total ratings for doctors on the platform do not always represent the unfiltered aggregate feedback of patients**. Hence, patients' choices will be biased. On average, premium profiles benefit from this and non-premium profiles are at a disadvantage.\n", "

\n" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.7.9" } }, "nbformat": 4, "nbformat_minor": 4 }