Uber Reviews Text Analysis. Exploratory Analysis, Bag of Words... | by Andrew Hershy | Towards Data Science

This project outlines a text-mining classification model using bag-of-words and logistic regression. We will attempt to understand the relationship between Uber text reviews and ride ratings. This is a great place to start if you’re relatively new to unstructured data analysis, yet have some experience with statistics and/or other classification experience.
Data Source: Kaggle by user Purvank
Preliminary AnalysisFormatting / Converting TextLogistic RegressionTesting / Conclusions
Preliminary Analysis
Formatting / Converting Text
Logistic Regression
Testing / Conclusions
First let’s bring in the data and visualize the dataframe:
#importing modulesimport pandas as pdimport numpy as npimport seaborn as sns%matplotlib inlineimport matplotlib.pyplot as pltimport random#pulling in datadf = pd.read_csv(r'C:\Users\Andrew\Desktop\Python Text Analysis\Uber_Ride_Reviews.csv')df
It’s a straightforward dataset. We have the “ride_review” text column and the “ride_rating” column (which ranges from 1 as the lowest rating and 5 as the highest). Users write these text reviews to describe their experience and the categorical 5 star rating summarizes it.
Let’s check the .describe() method to see how many instances we’re dealing with and other basic statistics:
df.describe()
It appears Uber drivers haven’t performed very well in this sample. The mean is a rating of 1.62, and the 75% mark is a 1 star review! This tells us there are a disproportionate amount of 1 star reviews. Hopefully we can gain insight into this by building a model.
Having mentioned that, let’s reiterate the utility of the bag of words / logistic regression model we will build:
We will be able to gauge the sentiment of future text reviews and categorize them into “Good” or “Bad” classes.
We will be able to pinpoint specific words with a high impact on rating sentiment. This information could be an asset for Uber (if used for their entire internal dataset — not this small sample we have). For example, the word “rude” likely has a negative coefficient, pushing our classifier to label that review as bad. The word “great” likely has a positive coefficient, pushing our classifier to label that review as good. The goal is discovery. We don’t know what interesting words may appear to have an impact on sentiment, and that’s the fun part. Does the word “great” outweigh “horrible” and net positive or negative? Many questions like this will be answered with our model.
Moving on, let’s check for null values in the dataset:
#checking for nullsnull_count = df.isnull().sum()null_count
None this time. If we had some nulls, we would need to explore how to deal with those. Many good resources exist on this topic you can research.
Next, let’s visualize the rating distribution of our data:
#seperating by groupsgroups = df.groupby('ride_rating').count()Values = groups.ride_reviewcolors = ['r', 'g', 'b', 'c', 'm']#making bar plotplt.bar(([1,2,3,4,5]), Values, color= colors)plt.title('Rating Distribution')plt.xlabel('Rating')plt.ylabel('Review Quantity')plt.show()
As we discovered earlier, there are a lot of 1 star reviews in this sample.
In order to perform the logistic regression later, we need to figure out a way to turn these 5 ratings categories into binary classes (1 and 0). Remember, logistic regression only handles “either/or” target variables. The best way to turn the star ratings into binary classes(in my opinion) is:
Set ratings below 3 stars as class 0 (negative sentiment)
Set ratings above 3 as class 1 (positive sentiment)
Delete the 3 star ratings. 3 stars are neutral and offer no sentiment insight.
So, let’s make a new column that deletes the 3 star ratings and creates a new column which classifies the other ratings into binary classes:
#deleting all instances with ride_rating = 3df = df[df.ride_rating != 3]#separating by groupsgroups = df.groupby('ride_rating').count()Values = groups.ride_reviewcolors = ['r', 'g', 'b', 'c']#making bar plotplt.bar(([1,2,4,5]), Values, color= colors)plt.title('Rating Distribution')plt.xlabel('Rating')plt.ylabel('Review Quantity')plt.show()
#creating new binary_class columndf['binary_class'] = np.where(df['ride_rating'] > 3, 1, 0)df
We are ready to proceed! Let’s get our text ready.
The first step in this process is separating our data into training and testing sets. We will create our model from the training data and save some instances for testing purposes later. We’re using sklearn to shuffle and split. Without messing with the parameters, it should split our data into 75% training and 25% testing.
By calling X_train.shape, we can check this. Further, let’s print a random review to verify it worked and to remind ourselves what we’re working with.
#splitting into train and testfrom sklearn.model_selection import train_test_splitX_train, X_test, y_train, y_test = train_test_split(df['ride_review'], df['binary_class'], random_state = 0)#setting random number between 1 and 1000number = random.randint(1,1000)#printing random training text and X_train shapeprint ('Random Review:')print(' ')print(X_train[number])print(' ')print('X_train shape: ' + str(X_train.shape))
As expected, 970/1294 = 75% of our sample.
Now comes the exciting part, translating our text data into numerical features. In order to run a regression though this data later, we need to have a feature for every word in our sample. Essentially, we will be translating each word to be assigned as a number and then counting the frequency of these word/numbers used per instance in matrices. The process of doing this is called “Bag-of-words”. It’s important to note that the order of words doesn’t matter, bag-of-words only counts the frequency per instance of words used. To start, we will use the Sklearn CountVectorizer
#importing countvectorizerfrom sklearn.feature_extraction.text import CountVectorizer#creating variable which assigns X_train to numbersvect = CountVectorizer().fit(X_train)#translates numbers back to textvect.get_feature_names()[1:10]
We can see using the len(vect.get_feature_names()) method, there are a total of 6,607 words in all the reviews:
#length of total wordslen(vect.get_feature_names())
Now, let’s transform our X_train data into a matrix which contains the documents (instances) as rows and the count of new features (6,607) as columns. For example, our first word (0) as seen above is “abandoned”. That will be the first column in the matrix. Whichever Uber reviews contain ‘abandoned’ will tally up how many times it was used and add it to that column. As a side note, someone using “abandoned”, will likely have a negative effect on rating. We can experiment later on this.
#creating matrix array for logistic regressionX_train_vectorized = vect.transform(X_train)print (X_train_vectorized.toarray())
Because there are so many words and most reviews only have a small portion of them, most of the numbers in this array will be 0.
We finally made it to the regression. If you need a logistic regression refresher before we proceed, please check here.
Again, we will be using sklearn to perform this model:
#creating log regressionfrom sklearn.linear_model import LogisticRegressionmodel = LogisticRegression()model.fit(X_train_vectorized, y_train)
Now, we will calculate the AUC to see how well it classifies the test data. More info on ROC curves and AUC can be found here:
#calculating AUCfrom sklearn.metrics import roc_auc_scorepredictions = model.predict(vect.transform(X_test))print('AUC: ', roc_auc_score(y_test, predictions))
This is good. A rough way of understanding this metric is to say we have 75% correctly classified instances.
Let’s delve into which words have the highest impact on class separation. Here we are translating our numbers back into words, getting the coefficients outputted by the regression, adding them both into a dataframe, and sorting them by their coefficient.
#creating array variable of all the wordsfeature_names = np.array(vect.get_feature_names())#creating array of all the regression coefficients per wordcoef_index = model.coef_[0]#creating df with both arrays in itdf = pd.DataFrame({'Word':feature_names, 'Coef': coef_index})#sorting by coefficientdf.sort_values('Coef')
This is fascinating to me.
“Charged” is the most negatively correlated word.
“Lyft” I thought was interesting as well. If customers have a negative experience, I’m sure they have a propensity to comment “Your competitor is better than you!”.
“Free” was one that confused me. I would have thought ‘free’ would be a positive-leaning word.
As you move down the dataframe, the words become more positively correlated.
“great” is the highest, with a score so positive, it even outweighs “charged”.
“Safe”, “Convenient”, “Clean”... a lot of these words are intuitively expected.
An unexpected positively correlated word is “she”. Do female drivers have a more positive effect on customer sentiment?
These results can provide potentially invaluable business insight to Uber’s stakeholders and leadership.
Lastly, we can experiment and test our own custom reviews. In bold below are the inputs and beneath that are the respective outputs (1 = positive; 0 = negative). That last one should easily be a positive, but our classifier labels it as negative. The rest seem accurate.
print(model.predict(vect.transform(['abandoned great'])))print(model.predict(vect.transform(['great she the best'])))print(model.predict(vect.transform(['charged slow horrible'])))print(model.predict(vect.transform(['it was as average as a trip could be'])))print(model.predict(vect.transform(['my family felt safe we got to our destination with ease'])))print(model.predict(vect.transform(['i got to my destination quickly and affordably i had a smile on my face from start to finish'])))
Thanks for reading.
I have a kernel on Kaggle with this analysis here
Also, if you want to connect, my LinkedIn is here
Please subscribe if you found this helpful. If you enjoy my content, please check out a few other projects:
Is Random Forest better than Logistic Regression? (a comparison)
Gini Index vs Information Entropy
Simple Linear vs Polynomial Regression
Predicting Cancer with Logistic Regression in Python
Bivariate Logistic Regression Example (python)
Calculating R-squared from scratch (using python)