Combining Data Science and Machine Learning with the Aviation Industry: A Personal Journey through a Capstone Project (Part I)
By: Christopher Kuzemka
Tags: Machine Learning, Aviation, Engineering, Covid 19, Data Science

Combining Data Science and Machine Learning with the Aviation Industry: A Personal Journey through a Capstone Project (Part I) Christopher Kuzemka Follow Jul 22 · 21 min read

Inspiration and Blog Summary

As a Mechanical Engineer turned Data Scientist, a large part of my time is spent on discovering ways in which both fields can be combined for the greater good. In my personal opinion, the mechanical engineering industry is in severe need of reform, through a manner that allows new and passionate graduates to directly implement their knowledge into industry, without the need for a PhD. Through this capstone project, I attempted to showcase such connections between engineering and data science by creating a potential business model used in aviation, by implementing a machine learning algorithm to solve a real-world problem, and by showcasing what issues are present when conducting such study during a pandemic. In this two-part series, we will uncover what is a standard data science workflow, research the airline and aviation industry, navigate through problems within our workflow in Python, expose ourselves to data collection APIs, and add as much engineering thought relevant to solving the problem expressed here. This series will read as a very personal story regarding the stress dealt with the project and what solutions were implemented to navigate around various obstacles. In this particular blog, we will discuss the thought process used in understanding the problem, discuss the APIs used as tools for data collection, discuss regime change, as well as showcase the process of data cleaning. Let’s begin by addressing the issue.

This Current Aviation Study

Consider the situation in which we as data scientists are approached by a business to answer the question: “What is the minimum cost threshold an airliner can charge their passengers per flight and how can we make a model that discovers this?” It isn’t uncommon for data scientists to be contracted out for freelance work; in this case, a startup business is trying to set itself up within the airline industry — a very unfortunate time to start up with the current pandemic….more on this later. The details on whether this business is an airliner or a third-party service working in conjunction with an airliner are unimportant, at the moment. The problem in its entirety is made up, which allows us some creativity in considering a solution.

So, what should a data scientist consider when approached with this question?

I would think the most direct approach in figuring out the minimum cost threshold is to analyze different flights that currently fly with different airlines. Every day, thousands of flights transpire above our heads around the world, which leaves us a plethora of data to work with in finding a solution. It may be possible to collect different features from these flights and observe different patterns across them, which may play a role in how airfares are determined for these flights. Our target label will be the price, implying that the models we plan to use here are supervised machine learning models. Our label, measured in U.S. dollars, is considered to be continuous — not in the infinite & uncountable mathematical sense, but in the computer scientist sense where there exist so many steps between a range of discrete values that it can be treated as continuous — which will ultimately mean that regression models will be best implemented. In working with pricing data and simultaneously addressing a business, we will want to optimize for a metric that makes most sense in describing the performance of the model. For this study, we will resort to using the mean absolute error due to its strength of being expressed in original units of our label and due to its resistance to being heavily affected by outliers. Other regression metrics could be used for optimization, but a data scientist should consider who his or her audience is and how much sense it will make to describe performance in a metric without any tangible significance; imagine using the mean squared error and expressing your model’s performance in squared dollars — doesn’t make much sense to speak like this, does it?

Now, what should a mechanical engineer consider when approached with this question?

I would focus on the details of the project. It may be worth looking into the distance traveled on flights, types of planes used, altitudes, weather patterns, time durations, travel trends throughout the year, and airlines. I would dive deeper, looking into technicalities such as force and energy output of the engines, temperature and pressure states across each cycle stage of the plane’s engines, changing fuel-mass ratio of the plane during a flight, average mass of passengers occupying the plane, changing air density of the surrounding environment, tail-wind effects, average fuel-burn rate during a plane’s taxi-time, and size of the plane. I would go further into analyzing other varying costs such as pilot and staff salary, maintenance cost, fuel-pricing, lawsuits, sponsoring partners, and stock trends.

How much time do we have for this study? When does this business need an answer by?

“This business needs a working proof of concept within 30 days.”

This is good to know when considering the scope of work present. The total project has the ability to scale astronomically with just the above possible features and methods mentioned; it would not be known until later if all of the features expressed here would be needed for our models, as some models perform worse when there are too many features and are considered to be overfit.

This time constraint is not enough time to make a thorough and effective investigation, but is enough to takeoff a working model and initial approach. It is now within our power to decide how we would like to approach the fake business. We will go over ways in which I approached this problem. We will see how the pandemic plays a role in our analysis and discover ways to maneuver around the data domain hurdles present.

What Access do we have to Data?

The FlightXML API

It was discovered that FlightAware’s FlightXML API would be a great tool to use to access the data we would need for this study. FlightAware.com is considered to be the most popular flight tracking website out there, providing details on all flights that have flown in recent history. It offers a very comprehensive API, used extensively by real businesses, to help individuals collect flight data in a very programmatic way. The tradeoff to using this API is that it is not a free tool to use. This will add some complexity to the study as we will want to gather as much data possible to help legitimize our study, while simultaneously keep the cost of the API’s usage down. Through the FlightXML API, we should have access to: weather data, altitude data, velocity data, aircraft type, flight schedules, distances, airlines, flight codes, origins, destinations, flight details on services, number of seats per aircraft, flight durations, etc.

The Skyscanner API

Skyscanner.com is a travel website meant to help people find the most affordable prices for hotels, flights, rentals, and more. Their [API] can be offered through approved developer consent. However, to eliminate the time spent negotiating with developers in establishing a secure and personalized API key, RapidAPI offers free use of the Skyscanner API, by acting as a host website for query searching. RapidAPI was extremely useful in that it only required a Google account to start using the API and offered the ability to perform “key rotations” — the act of discarding compromised API keys. There were some minor limitations in using RapidAPI’s access to Skyscanner’s API (such as the inability to access all of the commands available in the API’s documentation), but this did not prevent the ability to search prices of flights.

What Data do we have Access to?

The Skyscanner API

It took approximately 3–5 days to navigate the documentation of the Skyscanner API and understand its limitations. A programmatic way was needed to access various prices for flights and it didn’t take long to discover access to previous flights was not easily accomplished. Searching through different sites manually for “yesterday’s flight” was unsuccessful as no listings were available — perhaps this saves some server space for the airline business. The irony in this is that one can honestly plan a trip, months to years in advance, but will never be able to observe past flight pricing individually without having it being tracked some other way. This served as a complication towards our study because we had the intention of using past flight price trends to associate with past flight data, but even the API we were reliant on proved unsuccessful in this. The alternative suggestion was to take the flights gathered from the FlightXML API and replicate as many conditions as possible for each pricing in the future — that is, to make sure weekdays match up, months match up, airlines match up, etc. However, this approach also proved to be time-wasting as some flights gathered and observed within the FlightXML API could not be found across similar time periods, airlines, or on similar weekdays. Furthermore, the Skyscanner API lacked the ability to provide ticket pricing on a class basis, which hindered our ability to perform a more comprehensive study. The Skyscanner API will only search for the lowest price available, which was not the worst limitation as technically the solution we must provide is a minimum cost threshold model. This limitation is believed to be the reason why we would not be able to find class differentiation in pricing. In some cases, more than one price would be returned for a flight, but many of these prices are close together that their differences were negligible across carriers.

The FlightXML API

It took approximately 7+ days to learn how to programmatically navigate through the FlightXML API, understand its limitations, and discover solutions. To start, it was reminded that each search we performed costed “parts of a penny”, keeping us conscious that the tool must be navigated through efficiently. While “parts of a penny” does not sound like much money, it adds up when query searches are iteratively processed, and different sized data batches are always returned — often times, returning thousands of data points per query search. Costs and time penalties are typically expected with different APIs, as a user’s access is traffic and wear on a server. Our testing of the sophisticated API showed some searched data would be overlapped, some outputs gained from certain methods could not be pipelined as inputs to other methods, some searches returned null and still charged us money, some methods were deprecated, and some searches returned data which proved to be useless for our own purposes. To our dismay, we discovered that FlightXML API did not have the ability to search flight data past three months of the current date — at least with the non-subscription-based license we were paying for. This heavily limited the capabilities of our study as it is preferred to gather as many different flights as possible across a larger date range to show patterns with seasonality — no longer an available approach for us, here. To make matters more complicated, a different method available from the API, which could allow us to gather thousands of data points on individual altitudes, velocities, latitudes, longitudes, and position/technicalities of a previous flown flight, ONLY will search as far back as two weeks! If we wanted to capitalize on the ability to incorporate a thermal engine analysis in our model, we would have to sacrifice all aspects of a seasonal approach on our study. The initial approach taken was to choose six different routes, domestically within the US, and to analyze over a smaller timeframe. Even with these six flights chosen, there technically should be enough data to gather considering the six flights being analyzed are popular routes. According to Flights.com, “there are, on average 2,197 flights per month” made between JFK (John F. Kennedy International Airport) and MIA (Miami International Airport). This initially was one of the routes we planned to search. This meant we should be finding about 550 flights per week and 74 flights per day. When testing this claim, it was found that within a two-week span in the early chunk of May 2020, only about 14–15 flights were documented. To add more complexity, the method we could use to gather speed and altitude data on these flights would only return data for three of these flights! The reason behind this was because we were initially using an idealized departure coordinated universal time as an input for such method. Unfortunately, the method we wanted to use would only work best if we knew the actual departure coordinated universal time of the flight (and we didn’t have any immediate approach on how to programmatically search for actual departure times, due to unknown delays). To finalize the complexity, it was then found that the past days we wished to match up with future pricing on the Skyscanner API (parsing through to the exact date a year from the searched flight) would not return any pricing. This was thought to be due to differences in airlines flying for these future dates and possible differences in weekdays.

The above mentions on data access highlights only some of the complications found across both APIs integrating with one another. There were a few more aspects playing a role across the APIs, such as dealing with codeshare flights as duplicated searches and converting unique ICAO codes to IATA codes for airports and weather stations. While there may be a lot to juggle here, I have done my best to discuss most of the issues, but the largest one that stood out to me were the lack of flights within a two-week span on a popular route. Through some more testing, it was discovered that many flights were lacking worldwide, which heavily hindered our ability to gather all of the data needed for the study. It also increased usage cost of the FlightXML API as we were still being charged for searching through empty lists of flights — and searching through these empty lists was necessary to continue our study.

How Outside Elements Affect the Data Domain: The Covid-19 Pandemic

When historians analyze the year 2020, one of the biggest topics of discussion will be on the Covid-19 pandemic. This pandemic originated in Wuhan, China towards the end of 2019 and has persisted as a global threat since then. It affected a large majority of the world and continues to play a role in the way people live their lives, today. Ultimately, we have what is considered to be a “regime change”, which is a dramatic change to a way of life in society. Regime changes are very important to recognize in data science as they heavily affect the performance of machine learning models. If you are currently living in the pandemic, the way I am, you may notice how there are a large majority of individuals wearing masks, how establishments ask their customers to socially distance within their stores, and how more people have been carrying hand sanitizer with their belongings. Months before Covid-19 gained its notoriety, machine learning models, predicting the purchasing rates of masks and hand sanitizer, have been trained under a completely different regime; just as machine learning models which study the foot traffic of people in restaurants, subways, and in tourist attractions have been trained under a different regime. They are all inaccurate now with Covid-19 inducing a dramatic regime change in the previous patterns recognized.

Observe the below figure:

The blue line represents the total travelers tracked through TSA checkpoints around the world, from early March to late May. The foot traffic had decreased immensely when compared to the orange line, which showcases the TSA-tracked foot traffic a year earlier (same weekday). As a result, the airline industry in its entirety has suffered a major loss in profit. For solidified context on how individual airports were affected by the pandemic, refer to the below figure:

This decrease in foot traffic has much to do with the panic surrounding the pandemic as well as closures and limitations of travel. Coincidentally, the FlightXML API’s range of searches will almost entirely capture this lower traffic trend, leaving us with not much choice other than to conduct such study through this regime change. The benefit from doing this is that we will have a model working for a regime that has severely limited the airline business’ outreach; in case there were to be another possible world-wide disaster directly affecting the airline business, we technically would have a model that can predict through such disaster.

The Approach Taken

With all of the above considerations, the final approach taken to efficiently gather data and create a model was as follows:

Analyze airports of our choosing and all the flights currently flying around there. The following airports chosen were:

— — New York John F. Kennedy International — —

— — Chicago O’Hare International — —

— — Los Angeles International — —

— — Houston George Bush Intercontinental — —

— — Miami International — —

— — Hartsfield-Jackson Atlanta International — —

— — Portland International — —

These airports were chosen for their spread across the United States. With each search conducted through the FlightXML API, the 15 next flights approaching each airport were gathered. This search occurred at night, on May 27th 2020. Ultimately, we gathered 60 different flight combinations.

With each individual flight gathered, we would programmatically utilize the associated flight number as a unique identifier to gather data on its past scheduling for the month of May. By iterating in 8-hour increments on a flight number with the associated destination airport, we would be able to see all the times in which such flights occurred throughout the month of May. This helps us scale up our data collection process, by receiving a month’s worth of flight schedules for different flight combinations observed. We ultimately gathered about 5,000 individual flights.

From these flights, we would use ICAO flight code combinations and convert to IATA code flight combinations to programmatically gather prices of flights in the Skyscanner API (the Skyscanner API only accepts IATA code inputs).

Finally, we would have multiple dataframes constructed from the data collections to combine into one dataframe meant for modeling. We would hope that this method can match several different prices along with similar flights while, also match as many flights with pricing as possible in way that does not force us to drop data.

Data Cleaning Nuances

In every data scientist study, cleaning data will most often be the most time-consuming process undertaken. Data cleaning is important, in that it’s the main way to utilize any useful tools and methods available in your programming language; without proper input for methodologies, you’ll encounter a lot of error.

We won’t go over all of the cleaning necessary with this entire project other than highlighting some of the most arduous cleaning processes observed. Overall, a large amount of time was spent on cleaning the data. More of this cleaning process can be seen when looking at the source code folder of this project on my Github. In particular, we will look at some of the difficulties observed in cleaning the pricing dataframe gathered from Skyscanner API.

The Skyscanner API was extremely useful in being able to provide us a variety of quotes for the study, but the manner at which the data was received was not ideal for modeling whatsoever.

See the below two images respectively reflecting the head and tail of the dataframe:

Self-created image of a pricing dataframe’s head. Referenced source code.

Self-created image of a pricing dataframe’s tail. Referenced source code.

What we can take away from here is that some of our collected data returned empty. The “ValidationErrors” column would be an indicator that there was an error in receiving prices, but if this column is null, then it means a successful search was conducted. Ultimately, these empty rows stand for a search which was successfully conducted to find nothing (quite a useless feature in my personal opinion).

Simply removing the empty lists was no simple task either. Observe the following figure while pondering on the above indexed rows 715 and 717, for the next read:

Self-created image of a pricing dataframe’s head. Referenced source code.

The above output showcases information regarding this pricing dataframe. Each row, as shown in the above image, under the “Dtype” column for the dataframe information table, shows a data type of “object” (except in the unnecessary “Unnamed: 0” column, which is an “int” or integer). Further investigation has classified each cell in this dataframe as a “string”.

What we ultimately discovered in this cleaning process was that our pricing dataframe had each cell interpreted as a list of dictionaries, with keys meant to be interpreted as strings, where all such data types were entirely interpreted as strings. If you are an experienced programmer, you might certainly want to learn how to navigate through such “toxic dataframe” (as my data analytics friend puts it). Once the “empty string lists” were removed and all other nulls were removed, we ran the below function to convert the strings into what the interpreted datatypes should be:

import pandas as pd #imports the pandas package import ast #imports the ast package ## Converts the strings as literal expressions def take_as_literal(dataframe):

for column in dataframe.columns:

dataframe[column] = dataframe[column].apply(lambda element: ast.literal_eval(element)) #utilizes the ast package return dataframe

Self-created image of cleaned literal expressions in pricing dataframe. Referenced source code.

The above image showcases our new dataframe’s structure. We have taken each string as a literal expression through the ast package and now have a dataframe of lists containing dictionaries. Unfortunately, we seem to only have 110 rows of pricing. This is a strong foundation for future error in our model as we are now needing to consider what data have we lost in our cleaning and in our data gathering. Until we successfully merge our dataframes, we will not be able to recognize if we will have to drop more rows in our features dataframe for our flights. However, we can luckily at least recognize this dilemma by only noting the dramatic size different between our 5,000 row features dataframe and our 110 row pricing dataframe. Furthermore, there is some hope in how to salvage this study and how to navigate around this issue.

Note the next two images below regarding the pricing dataframe’s dynamic sizing:

Self-created image of each element’s contents in a single row of the pricing dataframe. Referenced source code.

Self-created image of each element’s contents in a different row of the pricing dataframe. Referenced source code.

Each individual row showcases different sized lists of dictionaries as elements. Each row is specific to an individual flight within a certain month. The above images show that we encounter multiple “low” prices for such flights across multiple carriers. The lists showcasing quotes information and carrier information dynamically change length throughout our dataframe. This may be enough to salvage our study as we are only relying on there being more than 60 flight combinations to keep our features data. If we are lucky, we would find many quotes per list for this pricing dataframe.

The below function was used to unpack the dataframe and create a final dataframe to be merged with our features for modeling. We ultimately also utilized the extra information details in this dataframe as features to add to our final model dataframe. We need not worry about this acting as a form of data leakage, as such features are also considered to be the types of data we could possibly find already within our 5,000 row flights dataframe.

def create_targetframe(price_dataframe): quotes = pd.DataFrame(columns = ['QuoteId', 'MinPrice', 'Direct', 'CarrierIds', 'OriginId', 'DestinationId', 'DepartureDate'])

places = pd.DataFrame(columns = ['PlaceId', 'IataCode', 'Name', 'CityName', 'CountryName'])

carriers = pd.DataFrame(columns = ['CarrierId', 'Name']) ## Makes the Quotes Dataframe

for i in range(len(price_dataframe)):

for j in range(len(price_dataframe.loc[i, 'Quotes'])):

quoteid = price_dataframe.loc[i, 'Quotes'][j]['QuoteId']

minprice = price_dataframe.loc[i, 'Quotes'][j]['MinPrice']

direct = price_dataframe.loc[i, 'Quotes'][j]['Direct']

carrierid = price_dataframe.loc[i, 'Quotes'][j]['OutboundLeg']['CarrierIds']

originid = price_dataframe.loc[i, 'Quotes'][j]['OutboundLeg']['OriginId']

destinationid = price_dataframe.loc[i, 'Quotes'][j]['OutboundLeg']['DestinationId']

departuredate = price_dataframe.loc[i, 'Quotes'][j]['OutboundLeg']['DepartureDate'] individual_quotes_dict = {'QuoteId':quoteid,

'MinPrice': minprice,

'Direct': direct,

'CarrierIds': carrierid,

'OriginId': originid,

'DestinationId': destinationid,

'DepartureDate': departuredate} individual_quotes_df = pd.DataFrame(individual_quotes_dict, columns = individual_quotes_dict.keys())

quotes = pd.concat([quotes, individual_quotes_df]) ## Makes the Places Dataframe

for i in range(len(price_dataframe)):

for j in range(len(price_dataframe.loc[i, 'Places'])):

placeid = price_dataframe.loc[i, 'Places'][j]['PlaceId']

iatacode = price_dataframe.loc[i, 'Places'][j]['IataCode']

name = price_dataframe.loc[i, 'Places'][j]['Name']

cityname = price_dataframe.loc[i, 'Places'][j]['CityName']

countryname = price_dataframe.loc[i, 'Places'][j]['CountryName']



individual_places_dict = {'PlaceId':placeid,

'IataCode':iatacode,

'Name':name,

'CityName':cityname,

'CountryName':countryname} individual_places_df = pd.DataFrame(individual_places_dict, columns = individual_places_dict.keys(), index = [j])

places = pd.concat([places, individual_places_df])

## Makes the Carriers DataFrame

for i in range(len(price_dataframe)):

for j in range(len(price_dataframe.loc[i, 'Carriers'])):

carrierid = price_dataframe.loc[i, 'Carriers'][j]['CarrierId']

name = price_dataframe.loc[i, 'Carriers'][j]['Name'] individual_carriers_dict = {'CarrierId': carrierid,

'Name': name} individual_carriers_df = pd.DataFrame(individual_carriers_dict, columns = individual_carriers_dict.keys(), index = [j])

carriers = pd.concat([carriers, individual_carriers_df])

## Cleans the Quotes DataFrame

quotes.drop_duplicates(inplace = True)

quotes.reset_index(inplace = True)

quotes.drop(columns = 'index', inplace = True)

quotes.rename(columns = {'CarrierIds':'CarrierId'}, inplace = True)

## Cleans the Places Dataframe

places.drop_duplicates(inplace = True)

places.reset_index(inplace = True)

places.drop(columns = 'index', inplace = True)

places['OriginId'] = places['PlaceId']

places['DestinationId'] = places['PlaceId']

places.drop(columns = 'PlaceId', inplace = True)





## Cleans the Carriers Dataframe

carriers.drop_duplicates(inplace = True)

carriers.reset_index(inplace = True)

carriers.drop(columns = 'index', inplace = True)

carriers.rename(columns = {'Name':'CarrierName'}, inplace = True) #Merging of three dataframes

quotes = pd.merge(quotes, right = places, how = 'inner', on = 'OriginId')

quotes.drop(columns = 'DestinationId_y', inplace = True)

quotes.rename(columns = {'DestinationId_x':'DestinationId', 'IataCode':'OriginIataCode','Name':'OriginName', 'CityName':'OriginCityName', 'CountryName':'OriginCountryName'}, inplace = True) quotes = pd.merge(quotes, right = places, how = 'inner', on = 'DestinationId')

quotes.drop(columns = 'OriginId_y', inplace = True)

quotes.rename(columns = {'OriginId_x':'OriginId', 'IataCode':'DestinationIataCode','Name':'DestinationName', 'CityName':'DestinationCityName', 'CountryName':'DestinationCountryName'}, inplace = True) quotes = pd.merge(quotes, right = carriers, how = 'inner', on = 'CarrierId') return quotes

When unpacking both of our pricing dataframes (one, which is not shown) and performing such cleaning, we ultimately found there to be 6,350 new rows of data — that is 6,350 individual quotes. This is less than what was ideally intended for our study, but is workable in that it is larger than our features dataframe. The more quotes we could gather, the more meaningful our study is as we would then be able to have a more data used for regression modeling. Too little quotes could force us to resort to classification modeling, which would not make much sense with regard to meaningful price prediction.

Bootstrapping

Bootstrapping is a useful practice often used in data science for understanding metrics such as standard error, for constructing confidence intervals, and for performing hypothesis testing on data.It is an extremely helpful technique. It resamples data randomly with replacement to generate multiple datasets, all stemming from a root data set. Such technique can be illustrated in the below image of a fake dataset:

Note that each column represents the original dataset in terms of length and elements used. While it may not contain all of the same unique elements within the original, it will never contain different unique elements separate from the set of original unique elements — that is to analogously say that each bootstrapped dataset of unique elements is a subset of the original dataset.

For our case, it was most wise to implement a bootstrapping technique onto our model dataframe. What we ultimately had was a feature dataframe and a target dataframe with no simple way of determining the meaningful count of different quotes per flight. In order to take full advantage of the various quotes which may exist for similar flights, a for loop consistently changing the order of prices observed in our target dataframe, while simultaneously following along the same index of our features dataframe was implemented to create a bootstrapped frame of prices of equal size to the flights dataframe. This adjusted quotes dataframe, was now ready to be fully integrated with our features and allowed us to begin modeling and data observation.

Final Words & Observing the Model Dataframe

Before we end this blog, and save the modeling and observation for the concluding Part II of this series, observe the final model dataframe shown below:

Self-created image of the model dataframe’s head. Referenced source code.

Self-created image of the model dataframe’s tail. Referenced source code.

Self-created image of the model dataframe’s columns. Referenced source code.

The dataframe is already quite large and we won’t be able to show the entire table in a single image, but the third image above of a list shows all the columns we included for our exploratory data analysis and our modeling. We will ultimately analyze and potentially use all of such features to understand the patterns observed in the “MinPrice” column. Our final modeling dataframe will include the one-hot encoding of certain columns containing strings, which will astronomically increase the size of our dataframe to around 4,100 columns. This one-hot encoding will be necessary for implementation into models, as all string-object data types will now be expressed as numerical inputs for the modeling.

To conclude, I would like to give a final thanks to everyone who followed me on my journey for this capstone. I thank you all for reading Part I of this journey and to please be on the look-out for Part II of this series. In Part II, we will dive into the visual analysis and supervised machine learning modeling of our data, which includes KNN Regression, Decision Tree Regression, as well as Ensemble Modeling. It was originally intended to showcase such conclusions within this blog, but ultimately decided that breaking the read into two segments would be best to not overwhelm ourselves with the information intake. Leaving us on this cliffhanger, ponder on which models could be best implemented into our study, how can we continue to navigate through previously discussed problems, and hypothesize which model would be best in minimizing the mean absolute error of its performance (hint: it will not be Linear Regression, which is often shown to be a very reliable regression model for pricing data). If you wish to explore the project in its entirety, the source code is linked here, on my Github. Note that some parts of this project are continuously being improved upon and it will certainly be conducted again in a post-Covid world.