Data Science for Fun and Motorsport
One of my favorite things about motorsport is telemetry. It’s a wealth of information that, with a human eye, is overwhelming. Data science or, more specifically, data visualization really breaks down data to something instantly digestible for consideration, analysis, and action.
Table Of Contents
Start your engines
Telemetry, and electronics in general, are vital to motorsport as we know it. Through data transmission and collection we can audit adherence to rule sets, learn about and diagnose issues while the vehicle is still out on the track, and iterate on vehicle performance with assistance from data analysis. Another component, which also strikes me as very interesting, is the ability for telemetry and data analysis to enrich the spectator experience.
All of these components are, perhaps, of equal interest to me however they all have a commonality that may not be obvious at first glace: financial cost. Motorsport, itself, is a costly endeavor and the telemetry systems and associated sensors and communications equipment are part and parcel with building and running the rest of the vehicle. As a surrogate I’ve taken to Forza Horizon 5, a newly released game for Xbox One, Xbox Series S/X, and PC. Forza Horizon is definitely a more “arcade” entry into racing games however it does use the same system as Forza Motorsport 7 for sending vehicle telemetry out to a listener. This is probably more appropriate as I used an Xbox One controller as opposed to a race wheel which, in my case, allows for more realistic input.
This is done over UDP and there are some specifications with regards to how the data is delivered but I skipped over creating my own listener and opted to use forza telemetry by Austin Baccus to record the data to CSV. This is a Windows-only electron application (by the looks of it–had some issues building dotnet 6 on my M1 MBA that I chose to not troubleshoot) that also provides a HUD. Very cool.
Fields
There is a wealth of information delivered by the Forza game engine to the telemetry endpoint. It is potentially overwhelming and this is very specifically simulation–a professional vehicle would have many more sensors being recorded if not also being transmitted back to the garage via wireless link.
IsRaceOn
TimestampMS
EngineMaxRpm
EngineIdleRpm
CurrentEngineRpm
AccelerationX
AccelerationY
AccelerationZ
VelocityX
VelocityY
VelocityZ
AngularVelocityX
AngularVelocityY
AngularVelocityZ
Yaw
Pitch
Roll
NormalizedSuspensionTravelFrontLeft
NormalizedSuspensionTravelFrontRight
NormalizedSuspensionTravelRearLeft
NormalizedSuspensionTravelRearRight
TireSlipRatioFrontLeft
TireSlipRatioFrontRight
TireSlipRatioRearLeft
TireSlipRatioRearRight
WheelRotationSpeedFrontLeft
WheelRotationSpeedFrontRight
WheelRotationSpeedRearLeft
WheelRotationSpeedRearRight
WheelOnRumbleStripFrontLeft
WheelOnRumbleStripFrontRight
WheelOnRumbleStripRearLeft
WheelOnRumbleStripRearRight
WheelInPuddleDepthFrontLeft
WheelInPuddleDepthFrontRight
WheelInPuddleDepthRearLeft
WheelInPuddleDepthRearRight
SurfaceRumbleFrontLeft
SurfaceRumbleFrontRight
SurfaceRumbleRearLeft
SurfaceRumbleRearRight
TireSlipAngleFrontLeft
TireSlipAngleFrontRight
TireSlipAngleRearLeft
TireSlipAngleRearRight
TireCombinedSlipFrontLeft
TireCombinedSlipFrontRight
TireCombinedSlipRearLeft
TireCombinedSlipRearRight
SuspensionTravelMetersFrontLeft
SuspensionTravelMetersFrontRight
SuspensionTravelMetersRearLeft
SuspensionTravelMetersRearRight
CarOrdinal
CarClass
CarPerformanceIndex
DrivetrainType
NumCylinders
PositionX
PositionY
PositionZ
Speed
Power
Torque
TireTempFl
TireTempFr
TireTempRl
TireTempRr
Boost
Fuel
Distance
BestLapTime
LastLapTime
CurrentLapTime
CurrentRaceTime
Lap
RacePosition
Accelerator
Brake
Clutch
Handbrake
Gear
Steer
NormalDrivingLine
NormalAiBrakeDifference
I realize now that this list may be difficult to scroll down so this will likely cause me to improve on the theme of this website by creating an expandable code fence of some type. But the impression holds true: it’s a lot of data and it’s coming in at, from my estimation, with one full record (every one of those fields) approximately every 116 milliseconds. That’s a significant amount of data for a human to go through. It would be a bit of a chore to go through the significance of every field at this moment so I will do so as they come up.
time = ford_falcon['TimestampMS']
difference = time.diff(periods=-1)
print (np.mean(difference))
That’s the strictly operational code that I used to find the difference and that bring about the discussion of how this data is structured and manipulated. Python is all the rage in data science for a reason–structures and packages exist to make this process as capable as it can be. If you thought I was going to say “painless” you were right, I was thinking that, but I didn’t want to lie. The learning curve is significant to someone who is inexperienced with Python and even greater to someone who is inexperienced with Python and data visualization (e.g. me).
Pandas 🐼
pandas is a widely used data analysis library and can be considered standard at this point. It provides functions and structures that will be leveraged heavily throughout this process, most specifically the DataFrame
, however there is no tremendous need to jump over and read their documentation–I plan on easing into every concept as it comes up through this journey. Matplotlib is another library that makes this all possible and is similarly standard in this realm. We will also be using NumPy and SciPy for some other functionality. If you would want to break off and read the documentation now I fear that you may get caught up in quite the tangled web–these libraries contain a large amount of functionality and it’s not always obvious how they’ll behave or how that functionality can be applied.
Let me see what spring is like on Jupyter
Jupyter Notebooks are a relatively new concept that’s, if you can dig it, a shareable shell. Jupyter Notebooks can be saved, shared, and messed around with. I’ll have a notebook up on Github afterwards. It is a great way to learn new languages or packages. This is a good example of one of those times. Homebrew conquers all things and it’s straightforward to install.
brew install jupyterlab
I had some issues with jupyterlab
on my M1 MacBook but on Linux it works like gangbusters. Since I’m not on my Linux box and I still want to work on my laptop. Jupyter Notebook isn’t particularly suited for that, though. SSH has the ability to forward a part and, as Jupyter Notebook likes to run only available to localhost. There are ways to get around this but it isn’t necessary for this.
Google’s Colaboratory
Google Colab is a product that leverages GCP to provide Jupyter (or Jupyter-comaptible–there’s a lot of nuance there with regards to IPython and various other back ends and functionality) runtimes and can load Jupyter Notebooks straight up while also being able to have some local storage or connect to your GCP storage or Google Drive. I’ve migrated my Jupyter Notebook over to Colab and all I got was this lousy blog post section.
Migration Blues
I didn’t start this project on Google Colab because, as is my wont, I jumped in to the deep end and pip3
didn’t find the package that I was wanting to work with, specifically. In looking for the quickest way forward I set up Jupyter Notebook at home and made progress only to find that Python, and specifically matplotlib, examples vary greatly and lack context. I like context. Context helps one make better decisions. So that’s why there’s an entire section here on moving from Jupyter to Colab.
Moving the files was a simple matter. Create a folder in your Google Drive and drop everything in there together. That’s my CSVs and the Jupyter .ipynb
file. Sign up for a free Google Colab account (or spend $9.99/mo for more premium access–free should be fine for what we’re doing here but for machine learning it may be worth your while to jump to that paid tier. I have some experience there and I’ll say that it may not necessarily be faster for all workloads but it will not dominate the use of your machine while it’s running so you are free to do other things and, as always, YMMV but I have some posts on that coming up).
Once your .ipynb
is on Google Drive and you’re signed up for Google Colab you can actually just double-click it from your Drive window. Obviously if you’re starting from scratch you won’t need to do that and can just create a new Notebook in Colab. To get access to your files from Google Drive you’ll need to add a mount
cell at the top like this:
from google.colab import drive
drive.mount('/content/drive')
This is in-built and will provide a link that you must visit to sign-in and receive a code to authorize the access of Colab to your Google Drive. If you’re very concerned about the contents of your Drive then there are a number of other ways to get your files in there (pulling them from Git being one of them) but just be aware of that as we navigate through directory structures. By clicking the “Refresh” button on the directory view on the left you’ll see a folder called drive
come up in the root. So now I’m going to go through and change all of my file paths to /content//drive/MyDrive/ForzaData
from what they were before,
import pandas as pd
from matplotlib import pyplot as plt
from matplotlib.pyplot import figure
# read csv file
amg_one = pd.read_csv('/content/drive/MyDrive/ForzaData/amg_one.csv')
z_car = pd.read_csv('/content/drive/MyDrive/ForzaData/240z_2.csv')
ford_falcon = pd.read_csv('/content/drive/MyDrive/ForzaData/ford_falcon.csv')
# display DataFrame
print(amg_one)
That should work swimmingly. If it doesn’t, definitely use the “refresh” button on your directory view to see what the pathname should be but once Drive is hooked up it should be cruise control to read and write to it. I’ll also say that writing to Drive is slow so use the scratch disk in your instance if you want to write out a lot of operations. pd.read_csv
takes in a CSV from disk and converts it directly to a DataFrame
. In online examples you’ll usually see a DataFrame
object represented as df
. You can think of a df
as a spreadsheet or a database table–“records” are rows and “fields” are columns. Records are all related to each other in some way and will tend to keep that relationship. The DataFrame (I won’t be doing the code block for it the whole time–it’ll be a lot–it’s just a normal word now) will be the primary structure to interact with the data that you have. It doesn’t work quite like a two-dimensional array but we’ll go through some of those operations as they come up.
The only other issue I had was that, seeming in this version of the libraries installed, the command ax.set_box_aspect(aspect = (1,1,1))
does not exist. That being said the plot still worked as expected (it’s a 3D plot–we’ll get there).
Graph it up, B
In this instance I have telemetry from three vehicles. I definitely want to filter the data so I get exactly what I’m looking for so I run the following as Forza sets the IsRaceOn
field to 1 during a race.
race_amg_one = amg_one.loc[(amg_one['IsRaceOn'] == 1)]
race_240z = z_car.loc[(z_car['IsRaceOn'] == 1)]
race_falcon = ford_falcon.loc[(ford_falcon['IsRaceOn'] == 1)]
This is a good time to bring up the issue of data sanitation. Forza doesn’t really do any organization for you and the best you get is lap counts and IsRaceOn. I took a few captures but the Ford Falcon FPV capture was the latest one so it was the one where I’d already made the mistakes of starting the recording too early and getting strange data. The lessons learned there were:
- Dirty data can make this task more difficult so having the cleanest data possible is ideal but sanitation is necessary to ensure accuracy
- Start recording right as you hit “Start Race” in Forza Horizon. You have a couple of seconds between hitting that and the race starting.
- Make sure your telemetry is actually recording. I had some issues with
forza-telemetry
only successfully recording the first session after opening it. I’ll poke around and see if I can find out what the problem is and how to remediate it but just know to check so you don’t need to repeat your data capture session. - Anomalous data in an arcade-style game is to be expected. It comes from the strangest places.
ford_falcon.loc
is a method that will help select records. All DataFrame objects have this method and it’s one of the bet ways to cherry pick or slice up data. By passing it the boolean expression ford_falcon['IsRaceOn'] == 1
the loc
function will evaluate that for every record and return a DataFrame where the condition is True
.
Let’s start with setting up a very basic pyplot object. Recall that we imported from matplotlib import pyplot as plt
. That’s a very particular Python convention–aliasing your imports. Pyplot also uses that plt
as somewhat of a singleton which is also a very Pythonic convention. This was all a bit confusing to me, initially. These aliases are also very standardized and readily used in examples without any context so just keep your wits about you and note the conventions.
fig, ax = plt.subplots()
ax.plot(race_falcon['CurrentRaceTime'],race_falcon['AccelerationX'], color="orange", label="Ford Falcon FPV")
ax.set_xlabel("Race Time")
ax.set_ylabel("Acceleration X")
ax.legend()
I realize now that yours may not be formatted quite like this but, in running through the notebook I changed the style at some point. As plt
is a singleton the style gets changed across the board until it’s changed back. I will discuss styles in a later post. But you’ll notice some things here that aren’t really desirable. The two that jump out at me are the unit of measure in the Y-axis and the race time being presented in what looks to be seconds. These are both obstacles that can be readily overcome.
Ticking away the moments that make up a dull race
Every index on an axis is called a “tick” in the parlance of matplotlib. There are definite ways to format a tick and, in this case, the conversion of seconds into the format of minutes:seconds would be ideal. We can start that process by defining a function that will perform that operation.
def racetime_fmt(x, y):
minutes, seconds = divmod(x, 60)
return "{:.0f}".format(minutes) + ':' + "{:.0f}".format(seconds)
Python is a very pithy language so this may be difficult to interpret but I’ll do my best to explain it. The name of the function is racetime_fmt
and the parameters, x
and y
, are the x
axis tick value or the y
axis tick value. I’m specifically wanting to format the x
axis so I just ignored y
for the time being. When you see minutes, seconds
that’s very strange (but it’s already appeared) and that’s when functions can return multiple values, or even objects, and is also extremely Pythonic. divmod
returns two values, the integer quotient and then the modulo (I need to look up that term to see if it’s correct) or remainder. Dividing x
, which is measured in seconds, by 60 will give us the number of minutes and then the remained will be the number of seconds left. The actual string is constructed in the return
statement by providing a string literal that’s the format that we want and applying it to the literal that is passed as a parameter (in this case the minutes
and seconds
values returned by divmod
).
There’s significant documentation on Python strings and their formatting however it’s simplest to point out that this will give us integers with, very specifically, 0
digits after the decimal in a floating point number. We apply this formatter to the x
axis with a simple declaration.
fig, ax = plt.subplots()
ax.plot(race_falcon['CurrentRaceTime'],race_falcon['AccelerationX'], color="orange", label="Ford Falcon FPV")
ax.set_xlabel("Race Time")
ax.set_ylabel("Acceleration X")
def racetime_fmt(x, y):
minutes, seconds = divmod(x, 60)
return "{:.0f}".format(minutes) + ':' + "{:.0f}".format(seconds)
ax.xaxis.set_major_formatter(tick.FuncFormatter(racetime_fmt))
ax.legend()
That function is a “functional formatter” for the tick
object and will be applied to “major ticks” as seen by the function name. There are minor ticks as well but they aren’t being addressed currently.
Nuthin but a g
thang
The second grievance is that the measurements on y
for acceleration are odd. We, as humans, tend to measure lateral acceleration–especially in vehicles–in the unit g
. A g
is the equivalent of the acceleration of planet Earth on objects on its surface. It’s literally one Earth “gravity” and you can think of the force on an object to be it’s weight on the surface of the earth if you’d like to. The acceleration of the Earth on objects is, for our purposes, 9.81 meters per second per second as 1 g. This means that 1 g of lateral acceleration means it feels like you would be pushed up against the side of the car with the same amount of force as your weight. DataFrames are very cool in that they can take what would ordinarily be operations on a singular thing and perform them on a row or column quickly.
race_falcon['AccelerationX'] / 9.81
0 -0.000418
1 0.000232
2 -0.002438
3 -0.001309
4 0.004816
...
2061 -0.579115
2062 -0.361294
2063 -0.224737
2064 -0.083242
2065 0.000677
Name: AccelerationX, Length: 2066, dtype: float64
This looks for normal for a measurement in g. What else can we see about this column?
print ( abs(race_falcon['AccelerationX']).min() / 9.81 )
print ( abs(race_falcon['AccelerationX']).max() / 9.81 )
print ( abs(race_falcon['AccelerationX']).mean() / 9.81 )
4.253134556574923e-07
5.471163608562692
1.021939719205032
Note that I used abs
to get the absolute value of the values in the column because Acceleration is a vector. According to the Forza documentation it’s lateral to the car (i.e from driver’s side to passenger’s side) with negative values being towards the left and positive values being towards the right. Using abs
makes certain that we examine the magnitude of these values and not their directionality. 5 g’s of lateral acceleration is the anomalous value I mentioned before and, through analysis, I learned where it came from. You won’t ever get 5 g’s of turning in a Class A Ford Falcon FPV under normal circumstances–that’s Formula 1 territory. The takeaway here is that this raw data needs to be converted from meters per second per second to g to make sense (in the way I’m thinking about it). It’s actually trivially easy to make this happen, thankfully.
import matplotlib.ticker as tick #this is a new import to allow us to access the ticks on the axis
fig, ax = plt.subplots()
ax.plot(race_falcon['CurrentRaceTime'],race_falcon['AccelerationX'] / 9.81, color="orange", label="Ford Falcon FPV")
ax.set_xlabel("Race Time (M:S)")
ax.set_ylabel("Acceleration X (g)")
def racetime_fmt(x, y):
minutes, seconds = divmod(x, 60)
return "{:.0f}".format(minutes) + ':' + "{:.0f}".format(seconds)
ax.xaxis.set_major_formatter(tick.FuncFormatter(racetime_fmt))
ax.legend()
Yes, I realize that 9.81 is a “magic number” and when you get into “code smells” and the like this is not a recommended practice but for the sake of brevity I just used it. This gets us a more readable plot. Go ahead and add those units in there, too, otherwise your Physics teacher will be pissed.
That’s just step 1 in plotting and data analysis but it’s already been a beefy post. Keep your eyes peeled for part 2.
./content/posts/data-science-for-fun.md 9:8 warning Use first person (such as Microsoft.FirstPerson 'my') sparingly. 9:143 error More than 3 commas! marktoso.TresComas 9:183 warning Consider removing 'really'. Microsoft.Adverbs 14:67 warning Try to avoid using Microsoft.We first-person plural like 'we'. 14:120 warning Try to avoid using Microsoft.We first-person plural like 'we'. 14:343 warning Use first person (such as Microsoft.FirstPerson 'me') sparingly. 14:349 warning Consider removing 'very'. Microsoft.Adverbs 16:1 warning Consider using 'these' instead Microsoft.Wordiness of 'All of these'. 16:60 warning Use first person (such as Microsoft.FirstPerson 'me') sparingly. 16:663 warning Use first person (such as ' I Microsoft.FirstPerson ') sparingly. 16:731 warning Use first person (such as Microsoft.FirstPerson 'my') sparingly. 16:821 warning Use first person (such as Microsoft.FirstPerson 'my') sparingly. 18:102 warning Use first person (such as ' I Microsoft.FirstPerson ') sparingly. 18:127 warning Use first person (such as Microsoft.FirstPerson 'my') sparingly. 18:373 warning Use first person (such as Microsoft.FirstPerson 'my') sparingly. 18:387 warning Use first person (such as ' I Microsoft.FirstPerson ') sparingly. 18:443 warning Consider removing 'Very'. Microsoft.Adverbs 21:96 error Use 'it's' instead of 'It is'. Microsoft.Contractions 21:102 warning Consider removing Microsoft.Adverbs 'potentially'. 21:139 warning Consider removing 'very'. Microsoft.Adverbs 109:1 warning Use first person (such as 'I Microsoft.FirstPerson ') sparingly. 109:88 warning Use first person (such as Microsoft.FirstPerson 'me') sparingly. 109:260 warning Use first person (such as Microsoft.FirstPerson 'my') sparingly. 109:519 warning Use first person (such as ' I Microsoft.FirstPerson ') sparingly. 115:12 warning Consider removing 'strictly'. Microsoft.Adverbs 115:42 warning Use first person (such as ' I Microsoft.FirstPerson ') sparingly. 115:300 warning Use first person (such as ' I Microsoft.FirstPerson ') sparingly. 115:371 warning Use first person (such as ' I Microsoft.FirstPerson ') sparingly. 115:559 error Use 'for example' instead of Microsoft.Foreign 'e.g.'. 115:564 warning Use first person (such as Microsoft.FirstPerson 'me') sparingly. 118:183 warning Consider removing 'heavily'. Microsoft.Adverbs 118:530 warning Try to avoid using Microsoft.We first-person plural like 'We'. 118:703 warning Use first person (such as ' I Microsoft.FirstPerson ') sparingly. 120:8 warning Use first person (such as Microsoft.FirstPerson 'me') sparingly. 121:216 warning Prefer 'afterward' over Microsoft.Terms 'afterwards'. 121:228 error Use 'it's' instead of 'It is'. Microsoft.Contractions 125:1 warning Use first person (such as 'I Microsoft.FirstPerson ') sparingly. 125:40 warning Use first person (such as Microsoft.FirstPerson 'my') sparingly. 125:100 warning Use first person (such as Microsoft.FirstPerson 'I'm') sparingly. 125:113 warning Use first person (such as Microsoft.FirstPerson 'my') sparingly. 125:129 warning Use first person (such as ' I Microsoft.FirstPerson ') sparingly. 125:154 warning Use first person (such as Microsoft.FirstPerson 'my') sparingly. 125:230 warning Consider using 'can' instead Microsoft.Wordiness of 'has the ability to'. 128:390 warning Use first person (such as Microsoft.FirstPerson 'my') sparingly. 128:431 warning Use first person (such as ' I Microsoft.FirstPerson ') sparingly. 131:1 warning Use first person (such as 'I Microsoft.FirstPerson ') sparingly. 131:60 warning Use first person (such as Microsoft.FirstPerson 'my') sparingly. 131:136 warning Use first person (such as ' I Microsoft.FirstPerson ') sparingly. 131:218 warning Use first person (such as ' I Microsoft.FirstPerson ') sparingly. 131:341 warning Consider removing 'greatly'. Microsoft.Adverbs 131:367 warning Use first person (such as ' I Microsoft.FirstPerson ') sparingly. 133:122 warning Use first person (such as Microsoft.FirstPerson 'my') sparingly. 133:274 warning Try to avoid using Microsoft.We first-person plural like 'we'. 133:477 error Use 'won't' instead of 'will Microsoft.Contractions not'. 133:597 warning Use first person (such as ' I Microsoft.FirstPerson ') sparingly. 140:157 warning Consider removing 'very'. Microsoft.Adverbs 140:341 warning Try to avoid using Microsoft.We first-person plural like 'we'. 140:513 warning Use first person (such as Microsoft.FirstPerson 'I'm') sparingly. 140:548 warning Consider using 'all' instead Microsoft.Wordiness of 'all of'. 140:555 warning Use first person (such as Microsoft.FirstPerson 'my') sparingly. 153:954 warning Try to avoid using Microsoft.We first-person plural like 'we'. 155:21 warning Use first person (such as ' I Microsoft.FirstPerson ') sparingly. 155:140 error Use 'doesn't' instead of 'does Microsoft.Contractions not'. 155:223 warning Try to avoid using Microsoft.We first-person plural like 'we'. 158:17 warning Use first person (such as ' I Microsoft.FirstPerson ') sparingly. 158:95 warning Use first person (such as ' I Microsoft.FirstPerson ') sparingly. 158:115 warning Use first person (such as Microsoft.FirstPerson 'I'm') sparingly. 158:133 warning Use first person (such as ' I Microsoft.FirstPerson ') sparingly. 164:77 warning Consider removing 'really'. Microsoft.Adverbs 164:160 warning Use first person (such as ' I Microsoft.FirstPerson ') sparingly. 167:186 warning Use first person (such as ' I Microsoft.FirstPerson ') sparingly. 172:1 warning Try to avoid using Microsoft.We first-person plural like 'Let's'. 172:31 warning Consider removing 'very'. Microsoft.Adverbs 172:69 warning Try to avoid using Microsoft.We first-person plural like 'we'. 172:130 warning Consider removing 'very'. Microsoft.Adverbs 172:259 warning Consider removing 'very'. Microsoft.Adverbs 172:317 warning Use first person (such as Microsoft.FirstPerson 'me') sparingly. 172:355 warning Consider removing 'very'. Microsoft.Adverbs 172:377 warning Consider removing 'readily'. Microsoft.Adverbs 182:1 warning Use first person (such as 'I Microsoft.FirstPerson ') sparingly. 182:99 warning Use first person (such as ' I Microsoft.FirstPerson ') sparingly. 182:223 warning Use first person (such as ' I Microsoft.FirstPerson ') sparingly. 182:310 warning Consider removing 'really'. Microsoft.Adverbs 182:353 warning Use first person (such as Microsoft.FirstPerson 'me') sparingly. 182:494 warning Consider removing 'readily'. Microsoft.Adverbs 185:211 warning Try to avoid using Microsoft.We first-person plural like 'We'. 191:13 warning Consider removing 'very'. Microsoft.Adverbs 191:83 warning Use first person (such as Microsoft.FirstPerson 'my') sparingly. 191:238 warning Use first person (such as Microsoft.FirstPerson 'I'm') sparingly. 191:288 warning Use first person (such as ' I Microsoft.FirstPerson ') sparingly. 191:367 warning Consider removing 'very'. Microsoft.Adverbs 191:491 warning Consider removing 'extremely'. Microsoft.Adverbs 191:708 warning Try to avoid using Microsoft.We first-person plural like 'us'. 191:903 warning Try to avoid using Microsoft.We first-person plural like 'we'. 191:942 error Use 'that's' instead of 'that Microsoft.Contractions is'. 193:178 warning Try to avoid using Microsoft.We first-person plural like 'us'. 193:196 warning Consider removing 'very'. Microsoft.Adverbs 193:272 warning Try to avoid using Microsoft.We first-person plural like 'We'. 210:80 warning Try to avoid using Microsoft.We first-person plural like 'We'. 210:453 warning Try to avoid using Microsoft.We first-person plural like 'our'. 210:687 warning Consider removing 'very'. Microsoft.Adverbs 210:811 warning Consider removing 'quickly'. Microsoft.Adverbs 227:61 warning Try to avoid using Microsoft.We first-person plural like 'we'. 237:10 warning Use first person (such as ' I Microsoft.FirstPerson ') sparingly. 237:338 warning Try to avoid using Microsoft.We first-person plural like 'we'. 237:461 warning Use first person (such as ' I Microsoft.FirstPerson ') sparingly. 237:503 warning Use first person (such as ' I Microsoft.FirstPerson ') sparingly. 237:717 error Don't spell out the number in Microsoft.Units 'from meters'. 237:782 warning Use first person (such as Microsoft.FirstPerson 'I'm') sparingly. 237:856 warning Consider removing Microsoft.Adverbs 'thankfully'. 252:100 error Use 'isn't' instead of 'is Microsoft.Contractions not'. 252:157 warning Use first person (such as ' I Microsoft.FirstPerson ') sparingly. 252:184 warning Try to avoid using Microsoft.We first-person plural like 'us'. 258:210 warning Use first person (such as ' I Microsoft.FirstPerson ') sparingly.✖ 9 errors, 105 warnings and 0 suggestions in 1 file.