Personal Background

During my job-search and some downtime I started the Google Data Analytics Certificate, more or less for fun, so here is the fictional capstone project. The whole data and scripts can be found here

Report on Bicycle Usage by Members and Non-Members

Goal

Use bike usage data to inform the marketing team on how to lead to a better conversion from casual users to annual members.

Questions that needs to be addressed:

  • Are there different usage patterns, do casual members use bikes more on weekdays or weekends?
  • How are bikes used (round-trip or one-way)?
  • Do casual and annual members differ in terms of trip-durations?

Data Sources

The data has been provided by divvy bikes here, under this license.

The data is not connected to the fictional company the report is for, but let’s pretend it is. Data description in the capstone protocol are outdated, so certain questions cannot be addressed as for example individual identifiers have been removed over time.

Furthermore, usage patterns of the last 12 months (which are the most actual) might not represent actual use, due to the COVID-19 pandemic.

Possible changes due to the pandemic:

  • Lockdowns (i.e. less bike use)
  • Higher use of bikes, to avoid public transportation

To not save the data in the repository, I have written a small bash script (getdata.sh) to create the data folder and download the monthly .csv files from the resource into it. Data from April 2020 to April 2021 will be retrieved.

The data folder will also be used to store cleaned and aggregate data files.

Data Preprocessing

I downloaded the monthly files, so we need to get a quick overlook over the columns and structures and, if necessary adapt column names etc.

To make the whole calculation a bit more efficient, I am already transforming dates to datetime and drop a few unneeded columns.

Additionally, I am also creating summary tables for each month to have a few consistency checks.

files = dir(path="data/", pattern = glob2rx("20*.csv"))

dataframes = list()
controls = list()

c <- 1

for (fname in files){
    print(fname)
    # All ready reduce the amount of data in the loaded data, to keep things clean,
    # for validation also change dates to datetime
    dataframes[[c]] <- fread(paste('data/', fname, sep=''), sep=',',
                             select=c('ride_id', 'started_at', 'ended_at', 'member_casual',
                                      'start_station_name', 'end_station_name')) %>%
        mutate(started_at = as_datetime(started_at), ended_at = as_datetime(ended_at))

    # Do some consistency checks
    controls[[c]] <- summarise(dataframes[[c]], min_s = min(started_at), max_s = max(started_at),
                                     min_e = min(ended_at), max_e = max(ended_at),
                                     member = n_distinct(member_casual),
                                     stations = n_distinct(start_station_name),
                                     n = n())
    c <- c + 1
}
bind_rows(controls)

There are a few suspiciously long end dates for rides and possibly a few datapoints missing in early June 2020. So we need to see what will happen there. Also in May and June rides are heavily increasing as compared to April (pandemic?).

# Combine data and save, so we can load a single df.
whole <- bind_rows(dataframes)
write_csv(whole, 'data/wholedata.csv')
# Now do a few calculations, which we will need for further uses and analysis.

data <- whole %>%
  mutate(month = month(started_at, label=TRUE),
         date = date(started_at),
         day = day(started_at),
         year = year(started_at),
         day_of_week = wday(whole$started_at, label=TRUE),
         start_station_name = as.character(start_station_name),
         end_station_name = as.character(end_station_name))

data$tripduration <- as.numeric(difftime(whole$ended_at, whole$started_at))
data$samestation <- data$start_station_name == data$end_station_name

Now, also given the data, a bit of data cleaning has still to be performed. There are negative values, and a few very, very long times (let’s keep them in first).

The data has been processed to remove trips that are taken by staff as they service and inspect the system; and any trips that were below 60 seconds in length (potentially false starts or users trying to re-dock a bike to ensure it was secure).

But according to the new information, we do not need to remove service trips, so there appears to be some data cleaning which had already been done.

Unfortunately - there are still some testing stations in there, which will now be removed.

# Remove negative trips and testing stations (as well as missing data)
test_stations <- c("hubbard_test_lws", "WATSON TESTING - DIVVY", "",
                   "HUBBARD ST BIKE CHECKING (LBS-WH-TEST)")
data_clean <- filter(data, tripduration > 60) %>%
  filter(! start_station_name %in% test_stations,
         !end_station_name %in% test_stations)

write_csv(data_clean, 'data/clean_data.csv')

Analysis and Visualization

# Read data directly, to save time.
data <- fread('data/clean_data.csv')

To keep this project a bit more simple, I will summarize the data in terms of descriptive statistics and rely on simple generalized linear models to come to conclusions.

First show a summary of the data

summary(data %>%
          select(member_casual, month, day_of_week, tripduration, samestation) %>%
          mutate(tripduration = tripduration / 60))

We see here in the member_casual column, that more trips have been conducted by subscribers, than by casual members, that the most trips have been done on Saturdays, that August and July have had the highest usage in general. That the average trip duration is about 27 minutes and the median duration 15. The distribution is heavily skewed, as there are a few very long trips in the dataset, that do not appear to be noise. I would assume that these heavy users rented the bike for a whole month, though there is the possibility, that something during check-out went wrong.

Statistical analysis

The first question I want to address here is whether week-days and membership status have a significant influence on trip duration.

data <- mutate(data, tripduration_min = tripduration / 60)

tripduration_model <- glm(tripduration_min ~ day_of_week * member_casual, data=data)

summ(tripduration_model)
MODEL INFO:
Observations: 3535941
Dependent Variable: tripduration_min
Type: Linear regression

MODEL FIT:
χ²(13) = 693803591.94, p = 0.00
Pseudo-R² (Cragg-Uhler) = 0.00
Pseudo-R² (McFadden) = 0.00
AIC = 48466417.58, BIC = 48466613.75

Standard errors: MLE
-----------------------------------------------------------------------
                                            Est.   S.E.   t val.      p
--------------------------------------- -------- ------ -------- ------
(Intercept)                                40.84   0.59    69.56   0.00
day_of_weekDo                               0.78   0.81     0.95   0.34
day_of_weekFr                               1.50   0.77     1.96   0.05
day_of_weekMi                              -1.23   0.82    -1.49   0.14
day_of_weekMo                               3.20   0.83     3.86   0.00
day_of_weekSa                               5.00   0.71     7.08   0.00
day_of_weekSo                               8.28   0.73    11.27   0.00
member_casualmember                       -25.76   0.72   -35.57   0.00
day_of_weekDo:member_casualmember          -0.80   1.01    -0.80   0.43
day_of_weekFr:member_casualmember          -1.03   0.97    -1.06   0.29
day_of_weekMi:member_casualmember           1.18   1.02     1.17   0.24
day_of_weekMo:member_casualmember          -3.07   1.03    -2.99   0.00
day_of_weekSa:member_casualmember          -2.26   0.92    -2.46   0.01
day_of_weekSo:member_casualmember          -5.39   0.96    -5.63   0.00
-----------------------------------------------------------------------

Estimated dispersion parameter = 52518.1 

For the analysis of round trips versus one way trips, I will rely on summary statistics.

data %>%
  group_by(member_casual, day_of_week) %>%
  summarise(Proportion = round(mean(samestation) * 100, 2))

What we see, is that members mostly do one-way trips, whereas casual rides do round-trips.

data %>%
  group_by(member_casual, samestation) %>%
  summarise("Median Tripduration" = median(tripduration_min))

Looking at median trip durations for members and one-way and round trips, we see that round-trips are about twice as long. And again longer for casual users than for members.

Finally, let’s look at differences in riding behavior.

data %>%
  group_by(member_casual, day_of_week) %>%
  summarise(n = n()) %>%
  mutate(Proportion = round(100 * (n / sum(n)), 2))
`summarise()` has grouped output by 'member_casual'. You can override using the `.groups` argument.

Here we also see a trend in that casual riders use bikes more often on the weekends, whereas members do not appear to have strong differences in weekly riding.

Report / Conclusions

Our population:

First of all, we see that in the last 12 months, there have been many more rides by members than by casual users.

However, due to data privacy, we cannot say how many riders we have so this dataset is incomplete in this regard.

Rides during the Week

`summarise()` has grouped output by 'member_casual'. You can override using the `.groups` argument.

`summarise()` has grouped output by 'member_casual'. You can override using the `.groups` argument.

Further interesting observations in terms of biking behavior are:

  • Casual members use bikes more often on the weekends
  • Members use bikes throughout the week
  • Casual members use bikes more often for round trips
  • Members do one-way trips

Trip durations

The graphic shows, also referring to our GLM analysis above, that casual rides do longer rides and that rides during the weekend are generally longer.

Conclusion and Advice

In my opinion given the data and having some experience in using bike-shares, I think the marketing team could consider the following three points to convert casual riders to annual riders:

LS0tDQp0aXRsZTogIkJpa2VzIFNoYXJlIChDYXBzdG9uZSkiDQpvdXRwdXQ6IGh0bWxfbm90ZWJvb2sNCi0tLQ0KDQojIFBlcnNvbmFsIEJhY2tncm91bmQNCkR1cmluZyBteSBqb2Itc2VhcmNoIGFuZCBzb21lIGRvd250aW1lIEkgc3RhcnRlZCB0aGUgR29vZ2xlIERhdGEgQW5hbHl0aWNzIENlcnRpZmljYXRlLCANCm1vcmUgb3IgbGVzcyBmb3IgZnVuLCBzbyBoZXJlIGlzIHRoZSBmaWN0aW9uYWwgY2Fwc3RvbmUgcHJvamVjdC4gDQoNCg0KIyBSZXBvcnQgb24gQmljeWNsZSBVc2FnZSBieSBNZW1iZXJzIGFuZCBOb24tTWVtYmVycw0KDQoNCiMjIEdvYWwNClVzZSBiaWtlIHVzYWdlIGRhdGEgdG8gaW5mb3JtIHRoZSBtYXJrZXRpbmcgdGVhbSBvbiBob3cgdG8gbGVhZCB0byBhIGJldHRlciBjb252ZXJzaW9uIGZyb20gY2FzdWFsIHVzZXJzIHRvIGFubnVhbCBtZW1iZXJzLg0KDQpRdWVzdGlvbnMgdGhhdCBuZWVkcyB0byBiZSBhZGRyZXNzZWQ6DQoNCiogQXJlIHRoZXJlIGRpZmZlcmVudCB1c2FnZSBwYXR0ZXJucywgZG8gY2FzdWFsIG1lbWJlcnMgdXNlIGJpa2VzIG1vcmUgb24gd2Vla2RheXMgb3Igd2Vla2VuZHM/DQoqIEhvdyBhcmUgYmlrZXMgdXNlZCAocm91bmQtdHJpcCBvciBvbmUtd2F5KT8NCiogRG8gY2FzdWFsIGFuZCBhbm51YWwgbWVtYmVycyBkaWZmZXIgaW4gdGVybXMgb2YgdHJpcC1kdXJhdGlvbnM/DQoNCiMjIERhdGEgU291cmNlcw0KVGhlIGRhdGEgaGFzIGJlZW4gcHJvdmlkZWQgYnkgZGl2dnkgYmlrZXMgW2hlcmVdKGh0dHBzOi8vZGl2dnktdHJpcGRhdGEuczMuYW1hem9uYXdzLmNvbS9pbmRleC5odG1sKSwgdW5kZXIgW3RoaXNdKGh0dHBzOi8vd3d3LmRpdnZ5YmlrZXMuY29tL2RhdGEtbGljZW5zZS1hZ3JlZW1lbnQpIGxpY2Vuc2UuIA0KDQpUaGUgZGF0YSBpcyBub3QgY29ubmVjdGVkIHRvIHRoZSBmaWN0aW9uYWwgY29tcGFueSB0aGUgcmVwb3J0IGlzIGZvciwgYnV0IGxldCdzIHByZXRlbmQgaXQgaXMuIERhdGEgZGVzY3JpcHRpb24gaW4gdGhlIGNhcHN0b25lIHByb3RvY29sIGFyZSBvdXRkYXRlZCwgc28gY2VydGFpbiBxdWVzdGlvbnMgY2Fubm90IGJlIGFkZHJlc3NlZCBhcyBmb3IgZXhhbXBsZSBpbmRpdmlkdWFsIGlkZW50aWZpZXJzIGhhdmUgYmVlbiByZW1vdmVkIG92ZXIgdGltZS4gDQoNCkZ1cnRoZXJtb3JlLCB1c2FnZSBwYXR0ZXJucyBvZiB0aGUgbGFzdCAxMiBtb250aHMgKHdoaWNoIGFyZSB0aGUgbW9zdCBhY3R1YWwpIG1pZ2h0IG5vdCByZXByZXNlbnQgYWN0dWFsIHVzZSwgZHVlIHRvIHRoZSBDT1ZJRC0xOSBwYW5kZW1pYy4gDQoNClBvc3NpYmxlIGNoYW5nZXMgZHVlIHRvIHRoZSBwYW5kZW1pYzoNCg0KKiBMb2NrZG93bnMgKGkuZS4gbGVzcyBiaWtlIHVzZSkNCiogSGlnaGVyIHVzZSBvZiBiaWtlcywgdG8gYXZvaWQgcHVibGljIHRyYW5zcG9ydGF0aW9uDQoNClRvIG5vdCBzYXZlIHRoZSBkYXRhIGluIHRoZSByZXBvc2l0b3J5LCBJIGhhdmUgd3JpdHRlbiBhIHNtYWxsIGJhc2ggc2NyaXB0IChgZ2V0ZGF0YS5zaGApIHRvIGNyZWF0ZSB0aGUgYGRhdGFgIGZvbGRlciBhbmQgZG93bmxvYWQgdGhlIG1vbnRobHkgYC5jc3ZgIGZpbGVzIGZyb20gdGhlIHJlc291cmNlIGludG8gaXQuIERhdGEgZnJvbSBBcHJpbCAyMDIwIHRvIEFwcmlsIDIwMjEgd2lsbCBiZSByZXRyaWV2ZWQuIA0KDQpUaGUgYGRhdGFgIGZvbGRlciB3aWxsIGFsc28gYmUgdXNlZCB0byBzdG9yZSBjbGVhbmVkIGFuZCBhZ2dyZWdhdGUgZGF0YSBmaWxlcy4NCg0KIyMgRGF0YSBQcmVwcm9jZXNzaW5nDQoNCmBgYHtyLCBlY2hvPUZBTFNFfQ0KIyBGb3VuZCB0aGlzIG5pY2UgYmxvZ3Bvc3Qgb24gZWZmaWNpZW50bHkgLyBsb2FkaW5nIGFuZCBpbnN0YWxsaW5nIHBhY2thZ2VzDQojIGh0dHBzOi8vc3RhdHNhbmRyLmNvbS9ibG9nL2FuLWVmZmljaWVudC13YXktdG8taW5zdGFsbC1hbmQtbG9hZC1yLXBhY2thZ2VzLyNpbmVmZmljaWVudC13YXktdG8taW5zdGFsbC1hbmQtbG9hZC1yLXBhY2thZ2VzDQojIExpYnJhcmllcw0KIyBQYWNrYWdlIG5hbWVzDQpwYWNrYWdlcyA8LSBjKCJnZ3Bsb3QyIiwgInNjYWxlcyIsICJ0aWR5dmVyc2UiLCAgImJyb29tIiwgImp0b29scyIsICJkYXRhLnRhYmxlIiwgImx1YnJpZGF0ZSIpDQoNCiMgSW5zdGFsbCBwYWNrYWdlcyBub3QgeWV0IGluc3RhbGxlZA0KaW5zdGFsbGVkX3BhY2thZ2VzIDwtIHBhY2thZ2VzICVpbiUgcm93bmFtZXMoaW5zdGFsbGVkLnBhY2thZ2VzKCkpDQppZiAoYW55KGluc3RhbGxlZF9wYWNrYWdlcyA9PSBGQUxTRSkpIHsNCiAgaW5zdGFsbC5wYWNrYWdlcyhwYWNrYWdlc1shaW5zdGFsbGVkX3BhY2thZ2VzXSkNCn0NCg0KIyBQYWNrYWdlcyBsb2FkaW5nDQppbnZpc2libGUobGFwcGx5KHBhY2thZ2VzLCBsaWJyYXJ5LCBjaGFyYWN0ZXIub25seSA9IFRSVUUpKQ0KYGBgDQoNCkkgZG93bmxvYWRlZCB0aGUgbW9udGhseSBmaWxlcywgc28gd2UgbmVlZCB0byBnZXQgYSBxdWljayBvdmVybG9vayBvdmVyIHRoZQ0KY29sdW1ucyBhbmQgc3RydWN0dXJlcyBhbmQsIGlmIG5lY2Vzc2FyeSBhZGFwdCBjb2x1bW4gbmFtZXMgZXRjLg0KDQpUbyBtYWtlIHRoZSB3aG9sZSBjYWxjdWxhdGlvbiBhIGJpdCBtb3JlIGVmZmljaWVudCwgSSBhbSBhbHJlYWR5IHRyYW5zZm9ybWluZw0KZGF0ZXMgdG8gZGF0ZXRpbWUgYW5kIGRyb3AgYSBmZXcgdW5uZWVkZWQgY29sdW1ucy4NCg0KQWRkaXRpb25hbGx5LCBJIGFtIGFsc28gY3JlYXRpbmcgc3VtbWFyeSB0YWJsZXMgZm9yIGVhY2ggbW9udGggdG8gaGF2ZSBhIGZldw0KY29uc2lzdGVuY3kgY2hlY2tzLg0KYGBge3J9DQpmaWxlcyA9IGRpcihwYXRoPSJkYXRhLyIsIHBhdHRlcm4gPSBnbG9iMnJ4KCIyMCouY3N2IikpDQoNCmRhdGFmcmFtZXMgPSBsaXN0KCkNCmNvbnRyb2xzID0gbGlzdCgpDQoNCmMgPC0gMQ0KDQpmb3IgKGZuYW1lIGluIGZpbGVzKXsNCiAgICBwcmludChmbmFtZSkNCiAgICAjIEFsbCByZWFkeSByZWR1Y2UgdGhlIGFtb3VudCBvZiBkYXRhIGluIHRoZSBsb2FkZWQgZGF0YSwgdG8ga2VlcCB0aGluZ3MgY2xlYW4sDQogICAgIyBmb3IgdmFsaWRhdGlvbiBhbHNvIGNoYW5nZSBkYXRlcyB0byBkYXRldGltZQ0KICAgIGRhdGFmcmFtZXNbW2NdXSA8LSBmcmVhZChwYXN0ZSgnZGF0YS8nLCBmbmFtZSwgc2VwPScnKSwgc2VwPScsJywNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc2VsZWN0PWMoJ3JpZGVfaWQnLCAnc3RhcnRlZF9hdCcsICdlbmRlZF9hdCcsICdtZW1iZXJfY2FzdWFsJywNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgJ3N0YXJ0X3N0YXRpb25fbmFtZScsICdlbmRfc3RhdGlvbl9uYW1lJykpICU+JQ0KICAgICAgICBtdXRhdGUoc3RhcnRlZF9hdCA9IGFzX2RhdGV0aW1lKHN0YXJ0ZWRfYXQpLCBlbmRlZF9hdCA9IGFzX2RhdGV0aW1lKGVuZGVkX2F0KSkNCg0KICAgICMgRG8gc29tZSBjb25zaXN0ZW5jeSBjaGVja3MNCiAgICBjb250cm9sc1tbY11dIDwtIHN1bW1hcmlzZShkYXRhZnJhbWVzW1tjXV0sIG1pbl9zID0gbWluKHN0YXJ0ZWRfYXQpLCBtYXhfcyA9IG1heChzdGFydGVkX2F0KSwgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbWluX2UgPSBtaW4oZW5kZWRfYXQpLCBtYXhfZSA9IG1heChlbmRlZF9hdCksIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIG1lbWJlciA9IG5fZGlzdGluY3QobWVtYmVyX2Nhc3VhbCksDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc3RhdGlvbnMgPSBuX2Rpc3RpbmN0KHN0YXJ0X3N0YXRpb25fbmFtZSksDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbiA9IG4oKSkNCiAgICBjIDwtIGMgKyAxDQp9DQoNCmBgYA0KDQpgYGB7cn0NCmJpbmRfcm93cyhjb250cm9scykNCmBgYA0KDQpUaGVyZSBhcmUgYSBmZXcgc3VzcGljaW91c2x5IGxvbmcgZW5kIGRhdGVzIGZvciByaWRlcyBhbmQgcG9zc2libHkgYSBmZXcgZGF0YXBvaW50cw0KbWlzc2luZyBpbiBlYXJseSBKdW5lIDIwMjAuIFNvIHdlIG5lZWQgdG8gc2VlIHdoYXQgd2lsbCBoYXBwZW4gdGhlcmUuDQpBbHNvIGluIE1heSBhbmQgSnVuZSByaWRlcyBhcmUgaGVhdmlseSBpbmNyZWFzaW5nIGFzIGNvbXBhcmVkIHRvIEFwcmlsIChwYW5kZW1pYz8pLg0KDQoNCmBgYHtyfQ0KIyBDb21iaW5lIGRhdGEgYW5kIHNhdmUsIHNvIHdlIGNhbiBsb2FkIGEgc2luZ2xlIGRmLg0Kd2hvbGUgPC0gYmluZF9yb3dzKGRhdGFmcmFtZXMpDQp3cml0ZV9jc3Yod2hvbGUsICdkYXRhL3dob2xlZGF0YS5jc3YnKQ0KYGBgDQoNCmBgYHtyfQ0KIyBOb3cgZG8gYSBmZXcgY2FsY3VsYXRpb25zLCB3aGljaCB3ZSB3aWxsIG5lZWQgZm9yIGZ1cnRoZXIgdXNlcyBhbmQgYW5hbHlzaXMuDQoNCmRhdGEgPC0gd2hvbGUgJT4lDQogIG11dGF0ZShtb250aCA9IG1vbnRoKHN0YXJ0ZWRfYXQsIGxhYmVsPVRSVUUpLA0KICAgICAgICAgZGF0ZSA9IGRhdGUoc3RhcnRlZF9hdCksDQogICAgICAgICBkYXkgPSBkYXkoc3RhcnRlZF9hdCksDQogICAgICAgICB5ZWFyID0geWVhcihzdGFydGVkX2F0KSwNCiAgICAgICAgIGRheV9vZl93ZWVrID0gd2RheSh3aG9sZSRzdGFydGVkX2F0LCBsYWJlbD1UUlVFKSwNCiAgICAgICAgIHN0YXJ0X3N0YXRpb25fbmFtZSA9IGFzLmNoYXJhY3RlcihzdGFydF9zdGF0aW9uX25hbWUpLA0KICAgICAgICAgZW5kX3N0YXRpb25fbmFtZSA9IGFzLmNoYXJhY3RlcihlbmRfc3RhdGlvbl9uYW1lKSkNCg0KZGF0YSR0cmlwZHVyYXRpb24gPC0gYXMubnVtZXJpYyhkaWZmdGltZSh3aG9sZSRlbmRlZF9hdCwgd2hvbGUkc3RhcnRlZF9hdCkpDQpkYXRhJHNhbWVzdGF0aW9uIDwtIGRhdGEkc3RhcnRfc3RhdGlvbl9uYW1lID09IGRhdGEkZW5kX3N0YXRpb25fbmFtZQ0KYGBgDQoNCk5vdywgYWxzbyBnaXZlbiB0aGUgZGF0YSwgYSBiaXQgb2YgZGF0YSBjbGVhbmluZyBoYXMgc3RpbGwgdG8gYmUgcGVyZm9ybWVkLiANClRoZXJlIGFyZSBuZWdhdGl2ZSB2YWx1ZXMsIGFuZCBhIGZldyB2ZXJ5LCB2ZXJ5IGxvbmcgdGltZXMgKGxldCdzIGtlZXAgdGhlbSBpbiBmaXJzdCkuDQoNCmBgYA0KVGhlIGRhdGEgaGFzIGJlZW4gcHJvY2Vzc2VkIHRvIHJlbW92ZSB0cmlwcyB0aGF0IGFyZSB0YWtlbiBieSBzdGFmZiBhcyB0aGV5IHNlcnZpY2UgYW5kIGluc3BlY3QgdGhlIHN5c3RlbTsgYW5kIGFueSB0cmlwcyB0aGF0IHdlcmUgYmVsb3cgNjAgc2Vjb25kcyBpbiBsZW5ndGggKHBvdGVudGlhbGx5IGZhbHNlIHN0YXJ0cyBvciB1c2VycyB0cnlpbmcgdG8gcmUtZG9jayBhIGJpa2UgdG8gZW5zdXJlIGl0IHdhcyBzZWN1cmUpLg0KYGBgDQoNCkJ1dCBhY2NvcmRpbmcgdG8gdGhlIG5ldyBpbmZvcm1hdGlvbiwgd2UgZG8gbm90IG5lZWQgdG8gcmVtb3ZlIHNlcnZpY2UgdHJpcHMsIHNvIHRoZXJlIGFwcGVhcnMgdG8gYmUgc29tZSBkYXRhIGNsZWFuaW5nIHdoaWNoIGhhZCBhbHJlYWR5IGJlZW4gZG9uZS4NCg0KVW5mb3J0dW5hdGVseSAtIHRoZXJlIGFyZSBzdGlsbCBzb21lIHRlc3Rpbmcgc3RhdGlvbnMgaW4gdGhlcmUsIHdoaWNoIHdpbGwgbm93IGJlIHJlbW92ZWQuDQoNCmBgYHtyfQ0KIyBSZW1vdmUgbmVnYXRpdmUgdHJpcHMgYW5kIHRlc3Rpbmcgc3RhdGlvbnMgKGFzIHdlbGwgYXMgbWlzc2luZyBkYXRhKQ0KdGVzdF9zdGF0aW9ucyA8LSBjKCJodWJiYXJkX3Rlc3RfbHdzIiwgIldBVFNPTiBURVNUSU5HIC0gRElWVlkiLCAiIiwgDQogICAgICAgICAgICAgICAgICAgIkhVQkJBUkQgU1QgQklLRSBDSEVDS0lORyAoTEJTLVdILVRFU1QpIikNCmRhdGFfY2xlYW4gPC0gZmlsdGVyKGRhdGEsIHRyaXBkdXJhdGlvbiA+IDYwKSAlPiUNCiAgZmlsdGVyKCEgc3RhcnRfc3RhdGlvbl9uYW1lICVpbiUgdGVzdF9zdGF0aW9ucywgDQogICAgICAgICAhZW5kX3N0YXRpb25fbmFtZSAlaW4lIHRlc3Rfc3RhdGlvbnMpDQoNCndyaXRlX2NzdihkYXRhX2NsZWFuLCAnZGF0YS9jbGVhbl9kYXRhLmNzdicpDQpgYGANCg0KIyMgQW5hbHlzaXMgYW5kIFZpc3VhbGl6YXRpb24NCg0KYGBge3J9DQojIFJlYWQgZGF0YSBkaXJlY3RseSwgdG8gc2F2ZSB0aW1lLg0KZGF0YSA8LSBmcmVhZCgnZGF0YS9jbGVhbl9kYXRhLmNzdicpDQpgYGANCg0KVG8ga2VlcCB0aGlzIHByb2plY3QgYSBiaXQgbW9yZSBzaW1wbGUsIEkgd2lsbCBzdW1tYXJpemUgdGhlIGRhdGEgaW4gdGVybXMgb2YgZGVzY3JpcHRpdmUgc3RhdGlzdGljcyBhbmQgcmVseSBvbiBzaW1wbGUgZ2VuZXJhbGl6ZWQgbGluZWFyIG1vZGVscyB0byBjb21lIHRvIGNvbmNsdXNpb25zLiANCg0KIyMjIEZpcnN0IHNob3cgYSBzdW1tYXJ5IG9mIHRoZSBkYXRhDQpgYGB7cn0NCnN1bW1hcnkoZGF0YSAlPiUgDQogICAgICAgICAgc2VsZWN0KG1lbWJlcl9jYXN1YWwsIG1vbnRoLCBkYXlfb2Zfd2VlaywgdHJpcGR1cmF0aW9uLCBzYW1lc3RhdGlvbikgJT4lIA0KICAgICAgICAgIG11dGF0ZSh0cmlwZHVyYXRpb24gPSB0cmlwZHVyYXRpb24gLyA2MCkpDQpgYGANCg0KV2Ugc2VlIGhlcmUgaW4gdGhlIGBtZW1iZXJfY2FzdWFsYCBjb2x1bW4sIHRoYXQgbW9yZSB0cmlwcyBoYXZlIGJlZW4gY29uZHVjdGVkIGJ5IHN1YnNjcmliZXJzLCB0aGFuIGJ5IGNhc3VhbCBtZW1iZXJzLCB0aGF0IHRoZSBtb3N0IHRyaXBzIGhhdmUgYmVlbiBkb25lIG9uIFNhdHVyZGF5cywgdGhhdCBBdWd1c3QgYW5kIEp1bHkgaGF2ZSBoYWQgdGhlIGhpZ2hlc3QgdXNhZ2UgaW4gZ2VuZXJhbC4gVGhhdCB0aGUgYXZlcmFnZSB0cmlwIGR1cmF0aW9uIGlzIGFib3V0IDI3IG1pbnV0ZXMgYW5kIHRoZSBtZWRpYW4gZHVyYXRpb24gMTUuIFRoZSBkaXN0cmlidXRpb24gaXMgaGVhdmlseSBza2V3ZWQsIGFzIHRoZXJlIGFyZSBhIGZldyB2ZXJ5IGxvbmcgdHJpcHMgaW4gdGhlIGRhdGFzZXQsIHRoYXQgZG8gbm90IGFwcGVhciB0byBiZSBub2lzZS4gSSB3b3VsZCBhc3N1bWUgdGhhdCB0aGVzZSBoZWF2eSB1c2VycyByZW50ZWQgdGhlIGJpa2UgZm9yIGEgd2hvbGUgbW9udGgsIHRob3VnaCB0aGVyZSBpcyB0aGUgcG9zc2liaWxpdHksIHRoYXQgc29tZXRoaW5nIGR1cmluZyBjaGVjay1vdXQgd2VudCB3cm9uZy4gDQoNCiMjIyBTdGF0aXN0aWNhbCBhbmFseXNpcw0KVGhlIGZpcnN0IHF1ZXN0aW9uIEkgd2FudCB0byBhZGRyZXNzIGhlcmUgaXMgd2hldGhlciB3ZWVrLWRheXMgYW5kIG1lbWJlcnNoaXAgc3RhdHVzIGhhdmUgYSBzaWduaWZpY2FudCBpbmZsdWVuY2Ugb24gdHJpcCBkdXJhdGlvbi4NCg0KYGBge3J9DQpkYXRhIDwtIG11dGF0ZShkYXRhLCB0cmlwZHVyYXRpb25fbWluID0gdHJpcGR1cmF0aW9uIC8gNjApDQoNCnRyaXBkdXJhdGlvbl9tb2RlbCA8LSBnbG0odHJpcGR1cmF0aW9uX21pbiB+IGRheV9vZl93ZWVrICogbWVtYmVyX2Nhc3VhbCwgZGF0YT1kYXRhKQ0KDQpzdW1tKHRyaXBkdXJhdGlvbl9tb2RlbCkNCmBgYA0KDQpGb3IgdGhlIGFuYWx5c2lzIG9mIHJvdW5kIHRyaXBzIHZlcnN1cyBvbmUgd2F5IHRyaXBzLCBJIHdpbGwgcmVseSBvbiBzdW1tYXJ5IHN0YXRpc3RpY3MuICANCmBgYHtyfQ0KZGF0YSAlPiUgDQogIGdyb3VwX2J5KG1lbWJlcl9jYXN1YWwsIGRheV9vZl93ZWVrKSAlPiUgDQogIHN1bW1hcmlzZShQcm9wb3J0aW9uID0gcm91bmQobWVhbihzYW1lc3RhdGlvbikgKiAxMDAsIDIpKQ0KYGBgDQoNCldoYXQgd2Ugc2VlLCBpcyB0aGF0IG1lbWJlcnMgbW9zdGx5IGRvIG9uZS13YXkgdHJpcHMsIHdoZXJlYXMgY2FzdWFsIHJpZGVzIGRvIHJvdW5kLXRyaXBzLg0KDQpgYGB7cn0NCmRhdGEgJT4lIA0KICBncm91cF9ieShtZW1iZXJfY2FzdWFsLCBzYW1lc3RhdGlvbikgJT4lIA0KICBzdW1tYXJpc2UoIk1lZGlhbiBUcmlwZHVyYXRpb24iID0gbWVkaWFuKHRyaXBkdXJhdGlvbl9taW4pKQ0KYGBgDQoNCkxvb2tpbmcgYXQgbWVkaWFuIHRyaXAgZHVyYXRpb25zIGZvciBtZW1iZXJzIGFuZCBvbmUtd2F5IGFuZCByb3VuZCB0cmlwcywgd2Ugc2VlIHRoYXQgcm91bmQtdHJpcHMgYXJlIGFib3V0IHR3aWNlIGFzIGxvbmcuIEFuZCBhZ2FpbiBsb25nZXIgZm9yIGNhc3VhbCB1c2VycyB0aGFuIGZvciBtZW1iZXJzLiANCg0KRmluYWxseSwgbGV0J3MgbG9vayBhdCBkaWZmZXJlbmNlcyBpbiByaWRpbmcgYmVoYXZpb3IuDQpgYGB7cn0NCmRhdGEgJT4lDQogIGdyb3VwX2J5KG1lbWJlcl9jYXN1YWwsIGRheV9vZl93ZWVrKSAlPiUNCiAgc3VtbWFyaXNlKG4gPSBuKCkpICU+JQ0KICBtdXRhdGUoUHJvcG9ydGlvbiA9IHJvdW5kKDEwMCAqIChuIC8gc3VtKG4pKSwgMikpDQpgYGANCg0KSGVyZSB3ZSBhbHNvIHNlZSBhIHRyZW5kIGluIHRoYXQgY2FzdWFsIHJpZGVycyB1c2UgYmlrZXMgbW9yZSBvZnRlbiBvbiB0aGUgd2Vla2VuZHMsIHdoZXJlYXMgbWVtYmVycyBkbyBub3QgYXBwZWFyIHRvIGhhdmUgc3Ryb25nIGRpZmZlcmVuY2VzIGluIHdlZWtseSByaWRpbmcuIA0KDQoNCiMgUmVwb3J0IC8gQ29uY2x1c2lvbnMNCg0KIyMgT3VyIHBvcHVsYXRpb246IA0KYGBge3IsIGVjaG89RkFMU0V9DQojIEZyb20gaHR0cDovL3d3dy5zdGhkYS5jb20vZW5nbGlzaC93aWtpL2dncGxvdDItcGllLWNoYXJ0LXF1aWNrLXN0YXJ0LWd1aWRlLXItc29mdHdhcmUtYW5kLWRhdGEtdmlzdWFsaXphdGlvbg0KDQpibGFua190aGVtZSA8LSB0aGVtZV9taW5pbWFsKCkrDQogIHRoZW1lKA0KICBheGlzLnRpdGxlLnggPSBlbGVtZW50X2JsYW5rKCksDQogIGF4aXMudGl0bGUueSA9IGVsZW1lbnRfYmxhbmsoKSwNCiAgcGFuZWwuYm9yZGVyID0gZWxlbWVudF9ibGFuaygpLA0KICBwYW5lbC5ncmlkPWVsZW1lbnRfYmxhbmsoKSwNCiAgYXhpcy50aWNrcyA9IGVsZW1lbnRfYmxhbmsoKSwNCiAgcGxvdC50aXRsZT1lbGVtZW50X3RleHQoc2l6ZT0xNCwgZmFjZT0iYm9sZCIpDQogICkNCg0KcGllIDwtIGRhdGEgJT4lIA0KICBncm91cF9ieShtZW1iZXJfY2FzdWFsKSAlPiUNCiAgc3VtbWFyaXNlKE51bWJlcnMgPSBuKCkpICU+JQ0KICBtdXRhdGUoUHJvcG9ydGlvbiA9IHJvdW5kKDEwMCAqIChOdW1iZXJzIC8gc3VtKE51bWJlcnMpKSwgMikpICU+JQ0KICByZW5hbWUoTWVtYmVyc2hpcCA9IG1lbWJlcl9jYXN1YWwpICU+JQ0KICBnZ3Bsb3QoYWVzKHg9IiIsIHk9UHJvcG9ydGlvbiwgZmlsbD1NZW1iZXJzaGlwKSkgKw0KICBnZW9tX2Jhcih3aWR0aCA9IDEsIHN0YXQgPSAiaWRlbnRpdHkiKSArIA0KICBibGFua190aGVtZSArDQogIGNvb3JkX3BvbGFyKCJ5Iiwgc3RhcnQ9MCkgKw0KICB0aGVtZShheGlzLnRleHQueD1lbGVtZW50X2JsYW5rKCkpICsNCiAgZ2VvbV90ZXh0KGFlcyh5ID0gUHJvcG9ydGlvbiAvIDIgKyBjKDAsIGN1bXN1bShQcm9wb3J0aW9uKVstbGVuZ3RoKFByb3BvcnRpb24pXSksIA0KICAgIGxhYmVsID0gcGVyY2VudChQcm9wb3J0aW9uIC8gMTAwKSksIHNpemU9NSkgKw0KICBsYWJzKHRpdGxlPSdQcm9wb3J0aW9uIG9mIFJpZGVzIGZvciBNZW1iZXJzIGFuZCBDYXN1YWwgUmlkZXJzJywNCiAgICAgICBzdWJ0aXRsZT0nRGF0YSBmcm9tIEFwcmlsIDIwMjAgdG8gQXByaWwgMjAyMSwgYmFzZWQgb24gdGhlIGRpdnZ5IHJpZGVzIGRhdGFzZXQuJykNCg0KcGllDQpgYGANCg0KRmlyc3Qgb2YgYWxsLCB3ZSBzZWUgdGhhdCBpbiB0aGUgbGFzdCAxMiBtb250aHMsIHRoZXJlIGhhdmUgYmVlbiBtYW55IG1vcmUgcmlkZXMgYnkgbWVtYmVycyB0aGFuIGJ5IGNhc3VhbCB1c2Vycy4gDQoNCkhvd2V2ZXIsIGR1ZSB0byBkYXRhIHByaXZhY3ksIHdlIGNhbm5vdCBzYXkgaG93IG1hbnkgcmlkZXJzIHdlIGhhdmUgc28gdGhpcyBkYXRhc2V0IGlzIGluY29tcGxldGUgaW4gdGhpcyByZWdhcmQuDQoNCiMjIFJpZGVzIGR1cmluZyB0aGUgV2Vlaw0KDQoNCmBgYHtyLCBlY2hvPUZBTFNFfQ0Kd2Vla19yaWRlcyA8LSBkYXRhICU+JQ0KICBncm91cF9ieShtZW1iZXJfY2FzdWFsLCBkYXlfb2Zfd2VlaykgJT4lDQogIHN1bW1hcmlzZShuID0gbigpKSAlPiUNCiAgbXV0YXRlKFByb3BvcnRpb24gPSByb3VuZCggKG4gLyBzdW0obikpLCAyKSkNCg0KcGllMiA8LSB3ZWVrX3JpZGVzICU+JSANCiAgcmVuYW1lKE1lbWJlcnNoaXAgPSBtZW1iZXJfY2FzdWFsLA0KICAgICAgICAgRGF5ID0gZGF5X29mX3dlZWspICU+JQ0KICBnZ3Bsb3QoYWVzKHg9ZmFjdG9yKERheSwgbGV2ZWw9YygnTW8nLCAnRGknLCAnTWknLCAnRG8nLCAnRnInLCAnU2EnLCAnU28nKSksIHk9UHJvcG9ydGlvbikpICsNCiAgZ2VvbV9iYXIoc3RhdD0iaWRlbnRpdHkiLCBhZXMoZmlsbD1NZW1iZXJzaGlwKSwgIHBvc2l0aW9uPXBvc2l0aW9uX2RvZGdlKHdpZHRoPTAuOSkpICsgDQogIGJsYW5rX3RoZW1lICsgDQogIGxhYnModGl0bGU9J1Byb3BvcnRpb24gb2YgUmlkZXMgQmFzZWQgb24gRGF5IG9mIHRoZSBXZWVrJywNCiAgICAgICBzdWJ0aXRsZT0nQ2FzdWFsIHJpZGVycyB1c2UgdGhlIHNlcnZpY2VzIG1vcmUgb2Z0ZW4gb24gdGhlIHdlZWtlbmQsIG1lbWJlcnMgdXNlIGl0IHRocm91Z2hvdXQuJykNCg0KcGllMg0KYGBgDQoNCmBgYHtyLCBlY2hvPUZBTFNFfQ0KZGF0YSAlPiUgDQogIGdyb3VwX2J5KG1lbWJlcl9jYXN1YWwsIGRheV9vZl93ZWVrKSAlPiUgDQogIHN1bW1hcmlzZShQcm9wb3J0aW9uID0gcm91bmQobWVhbihzYW1lc3RhdGlvbikgKiAxMDAsIDIpKSAlPiUNCiAgcmVuYW1lKE1lbWJlcnNoaXAgPSBtZW1iZXJfY2FzdWFsLA0KICAgICAgICAgRGF5ID0gZGF5X29mX3dlZWspICU+JQ0KICBnZ3Bsb3QoYWVzKHg9ZmFjdG9yKERheSwgbGV2ZWw9YygnTW8nLCAnRGknLCAnTWknLCAnRG8nLCAnRnInLCAnU2EnLCAnU28nKSksIHk9UHJvcG9ydGlvbikpICsNCiAgZ2VvbV9iYXIoc3RhdD0iaWRlbnRpdHkiLCBhZXMoZmlsbD1NZW1iZXJzaGlwKSwgIHBvc2l0aW9uPXBvc2l0aW9uX2RvZGdlKHdpZHRoPTAuOSkpICsgDQogIGJsYW5rX3RoZW1lICsgDQogIGxhYnModGl0bGU9J1Byb3BvcnRpb24gb2YgUm91bmQtVHJpcHMgRHVyaW5nIHRoZSBXZWVrJywNCiAgICAgICBzdWJ0aXRsZT0nQ2FzdWFsIHJpZGVycyB1c2UgYmlrZXMgbW9yZSBmb3Igcm91bmR0cmlwcy4nKQ0KDQoNCmBgYA0KDQpGdXJ0aGVyIGludGVyZXN0aW5nIG9ic2VydmF0aW9ucyBpbiB0ZXJtcyBvZiBiaWtpbmcgYmVoYXZpb3IgYXJlOg0KDQoqIENhc3VhbCBtZW1iZXJzIHVzZSBiaWtlcyBtb3JlIG9mdGVuIG9uIHRoZSB3ZWVrZW5kcw0KKiBNZW1iZXJzIHVzZSBiaWtlcyB0aHJvdWdob3V0IHRoZSB3ZWVrDQoqIENhc3VhbCBtZW1iZXJzIHVzZSBiaWtlcyBtb3JlIG9mdGVuIGZvciByb3VuZCB0cmlwcw0KKiBNZW1iZXJzIGRvIG9uZS13YXkgdHJpcHMgDQoNCg0KIyMgVHJpcCBkdXJhdGlvbnMNCg0KYGBge3IsIGVjaG89RkFMU0V9DQpkYXRhICU+JSANCiAgcmVuYW1lKE1lbWJlcnNoaXAgPSBtZW1iZXJfY2FzdWFsLA0KICAgICAgICAgRGF5ID0gZGF5X29mX3dlZWspICU+JQ0KICBmaWx0ZXIodHJpcGR1cmF0aW9uX21pbiA8IHF1YW50aWxlKHRyaXBkdXJhdGlvbl9taW4sIDAuOTUpKSAlPiUNCiAgZ2dwbG90KGFlcyh4PWZhY3RvcihEYXksIGxldmVsPWMoJ01vJywgJ0RpJywgJ01pJywgJ0RvJywgJ0ZyJywgJ1NhJywgJ1NvJykpLCB5PXRyaXBkdXJhdGlvbl9taW4pKSArDQogIGdlb21fYm94cGxvdChhZXMoZmlsbD1NZW1iZXJzaGlwKSkgKyANCiAgdGhlbWVfbWluaW1hbCgpICsgDQogIGxhYnModGl0bGU9J1RyaXAgRHVyYXRpb24gb2YgUmlkZXMgQmFzZWQgb24gRGF5IG9mIHRoZSBXZWVrJywNCiAgICAgICBzdWJ0aXRsZT0nVHJpcCBkdXJhdGlvbiB3YXMgY2FwcGVkIGF0IHRoZSA5NXRoIHBlcmNlbnRpbGUsIGFzIGRpc3RyaWJ1dGlvbnMgYXJlIGhlYXZpbHkgc2tld2VkIHRvIHRoZSBsZWZ0LicsDQogICAgICAgeT0nVHJpcCBEdXJhdGlvbiBpbiBNaW51dGVzJywNCiAgICAgICB4PSIiKQ0KDQpgYGANCg0KVGhlIGdyYXBoaWMgc2hvd3MsIGFsc28gcmVmZXJyaW5nIHRvIG91ciBHTE0gYW5hbHlzaXMgYWJvdmUsIHRoYXQgY2FzdWFsIHJpZGVzIGRvIGxvbmdlciByaWRlcw0KYW5kIHRoYXQgcmlkZXMgZHVyaW5nIHRoZSB3ZWVrZW5kIGFyZSBnZW5lcmFsbHkgbG9uZ2VyLg0KDQoNCiMgQ29uY2x1c2lvbiBhbmQgQWR2aWNlDQoNCkluIG15IG9waW5pb24gZ2l2ZW4gdGhlIGRhdGEgYW5kIGhhdmluZyBzb21lIGV4cGVyaWVuY2UgaW4gdXNpbmcgYmlrZS1zaGFyZXMsIEkgdGhpbmsgdGhlIG1hcmtldGluZyB0ZWFtIGNvdWxkIGNvbnNpZGVyIHRoZSBmb2xsb3dpbmcgdGhyZWUgcG9pbnRzIHRvIGNvbnZlcnQgY2FzdWFsIHJpZGVycyB0byBhbm51YWwgcmlkZXJzOg0KDQoqIENhdGVyIHRvIHRoZSBncmVhdCBuZXR3b3JrIG9mIHN0YXRpb25zIGFuZCBleHBsYWluIHRoYXQgdXNpbmcgYmlrZXMgZm9yIHNob3J0ZXIgdHJpcHMgaXMgZGVmaW5pdGVseSB3b3J0aCBpdC4gQXMgYSBtZW1iZXIgeW91IGRvIG5vdCBuZWVkIHRvIGNhcnJ5IHlvdXIgYmlrZSB3aXRoIHlvdSBhbGwgZGF5LCB5b3UgY2FuIGNoYW5nZSBpdCBqdXN0IGxpa2UgdGhhdC4NCiogRXNwZWNpYWxseSBkdXJpbmcgdGhlIHBhbmRlbWljOiBUaGUgbmV0d29yayBvZiBiaWtlIHN0YXRpb25zIGlzIGEgaGVhbHRoaWVyIGFsdGVybmF0aXZlIHRvIHB1YmxpYyB0cmFuc3BvcnRhdGlvbi4gSXQgY2FuIGFsc28gYmUgdXNlZCBmb3IgdGhlIGN1c3RvbWVycyBjb21tdXRlLg0KKiBNb3JlIGdlbmVyYWxseTogU3RyZXNzIHRoYXQgYW4gYW5udWFsIHN1YnNjcmlwdGlvbiBpcyBjaGVhcGVyIGFuZCByZWR1Y2VzIHN0cmVzcywgYXMgeW91IGNhbiBhbHdheXMgY2hvc2UgYSBiaWtlLCB3aGVyZXJ2ZXIgeW91IHdhbnQgYW5kIGRvIHlvdXIgbmV4dCB0cmlwIHdpdGggaXQgLSBOb3Qgb25seSBvbiB0aGUgd2Vla2VuZCBidXQgYWxzbyBmb3IgY29tbXV0ZXMgb3IgZ3JvY2VyeSBzaG9wcGluZyBkdXJpbmcgdGhlIHdlZWsuIA==