Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding Survey Responses to Public Dashboard #124

Merged
merged 75 commits into from
May 8, 2024
Merged
Show file tree
Hide file tree
Changes from 44 commits
Commits
Show all changes
75 commits
Select commit Hold shift + click to select a range
758d2d8
first draft of survey_responses notebook
Mar 13, 2024
a526257
add quality text to pie charts
Mar 15, 2024
c4cfdfa
revise the way surveys are read
Mar 20, 2024
427f44b
update dictionary building
Mar 20, 2024
94f48c5
only display questions currently in the survey
Mar 20, 2024
7f3d477
connect charts to frontend
Mar 22, 2024
74768f7
translate answers, drop zero other
Mar 22, 2024
99e0cc8
updates while working with washingtoncommons
Mar 22, 2024
eb14800
add check to prevent showing "label" questions
Mar 25, 2024
3a3533d
clean up some print statements
Mar 25, 2024
f832c83
drop columns that don't represent questions
Mar 25, 2024
dc3bab8
fix failed charts and alt text
Mar 26, 2024
ea0c660
no need to drop columns
Mar 26, 2024
157ad15
sensed instead of labeled trips by mode
Mar 26, 2024
e6c7fe6
pull out "input" type questions
Mar 29, 2024
8ac86b7
draft "trips presented" changes
Apr 2, 2024
5fe10e1
increase accuracy of quality text
Apr 3, 2024
7d8a114
bypass evals for non-conditional surveys
Apr 3, 2024
8a2c2f4
flatten entire row instead of pulling values
Apr 3, 2024
a04a316
add eval string as a parameter to filter fncn
Apr 3, 2024
af3dfa8
add the survey info parameter
Apr 3, 2024
399c7b3
add new notebook to crontab
Apr 3, 2024
75d4fb6
deal with missing data
Apr 3, 2024
45026b7
clean up notebook
Apr 3, 2024
733e559
remove old code
Apr 3, 2024
8a4869b
no "input" type questions in html
Apr 4, 2024
df6fde3
edits to quality text
Apr 5, 2024
d6b2d80
add sensed metrics to survey dashboards
Apr 9, 2024
0da6c33
comments for every case of dashboard
Apr 9, 2024
8ff0b8b
break survey notebook if irrelevant
Apr 9, 2024
8cdd6de
filter trips - required to keep out test users
Apr 9, 2024
4ea4724
debug df per survey
Apr 9, 2024
eff012b
use emcommon to determine survey prompted
Apr 17, 2024
4d0f7fb
Merge remote-tracking branch 'upstream/main' into dashboard-surveys
Apr 18, 2024
6c7496f
add color mapping to surveys
Apr 18, 2024
1e41c35
fix label translations
Apr 18, 2024
80263ad
add informative print statements
Apr 22, 2024
c98b623
add emcommon as a dependency to the yml file
Apr 22, 2024
10fe708
move if conditions
Apr 22, 2024
1b88265
choose a more distinct color pallete
Apr 22, 2024
1a7b911
clean up inputs cell
Apr 22, 2024
0fae9fc
change condition for the debug dataframe
Apr 23, 2024
f1e94f5
missing punctuation - fix corrupt notebook
Apr 23, 2024
61fa0f1
Merge remote-tracking branch 'AnantasCode/Change-from-Pie-Charts-to-1…
Abby-Wheelis May 4, 2024
3fd5d01
Merge remote-tracking branch 'AnantasCode/Change-from-Pie-Charts-to-1…
Abby-Wheelis May 5, 2024
e46a4b6
Merge remote-tracking branch 'AnantasCode/Change-from-Pie-Charts-to-1…
Abby-Wheelis May 5, 2024
0f31ed6
switch from composite to confirmed trips
Abby-Wheelis May 5, 2024
596886a
update translation of options - issues with numbers
Abby-Wheelis May 5, 2024
8d0e285
update color mapping
Abby-Wheelis May 5, 2024
130b732
Merge remote-tracking branch 'upstream/main' into dashboard-surveys
Abby-Wheelis May 6, 2024
fd16cad
introduce workaround for missing colors
Abby-Wheelis May 6, 2024
52bc509
add todo comments, remove old code
Abby-Wheelis May 6, 2024
548a449
update how the dataframe is formed
Abby-Wheelis May 6, 2024
7e64d12
update dataframe create
Abby-Wheelis May 6, 2024
feb80dc
bump up the emcommon version to latest release
Abby-Wheelis May 7, 2024
2dbd723
reintroduce "assigned" surveys, update quality text
Abby-Wheelis May 7, 2024
b02c1de
update import conventions
Abby-Wheelis May 7, 2024
5e79051
remove outdated code
Abby-Wheelis May 7, 2024
7dbef2d
update import styles
Abby-Wheelis May 7, 2024
39d1156
remove reliance on "total" sets
Abby-Wheelis May 7, 2024
7c7a718
push legend below chart if the first label is long
Abby-Wheelis May 7, 2024
6c38e97
update frontend for metrics
Abby-Wheelis May 7, 2024
a57a30f
remove olde code
Abby-Wheelis May 7, 2024
b207efa
create survey_metrics notebook
Abby-Wheelis May 7, 2024
6957500
add survey metrics to the front end
Abby-Wheelis May 7, 2024
2e2e269
remove testing-only code
Abby-Wheelis May 7, 2024
f9f3ba5
use correct debug frame
Abby-Wheelis May 7, 2024
3d4c5fb
restore default parameters
Abby-Wheelis May 7, 2024
40c1478
add survey metrics to list of stacked
Abby-Wheelis May 7, 2024
6a36770
handle empty dataframes better
Abby-Wheelis May 7, 2024
41db5cf
handle "Other" values in color map
Abby-Wheelis May 7, 2024
fb7bc70
tidy code
Abby-Wheelis May 7, 2024
334c95b
🔥 Remove git and em-common since they are in the server repo already
shankari May 8, 2024
ef4786e
⬆️ Upgrade base image
shankari May 8, 2024
91cc8e7
♻️ Minor fixes to the survey metrics
shankari May 8, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 64 additions & 1 deletion frontend/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,12 @@
const end_year = date.getFullYear();
var current_month = start_month;
var current_year = start_year;

//testing dfc fermata .. doesn't start until April...
// if ((current_month >= end_month) && (current_year >= end_year)) {
// current_month = current_month - 2; //dfc has not started yet...
// }

dates.push([current_month, current_year]);
while (!(current_month == end_month && current_year == end_year)) {
current_month += 1;
Expand All @@ -356,6 +362,28 @@
};
return dates;
};

function getDictionaryList(form_list) {
var quest_dict = {};
return new Promise(async (resolve) => {
for (i in form_list) {
response = await fetch(form_list[i]);
text = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(text, "text/xml");
labels = doc.getElementsByTagName("label");
for (i in labels) {
try {
if ((labels[i].parentNode.getAttribute("appearance") !== "label" && labels[i].parentNode.nodeName != "input")) //label type questions don't ever have answers
{
quest_dict[labels[i].parentNode.getAttribute("ref").split('/').slice(-1)] = labels[i].firstChild.data;
}
} catch (e) { }
}
}
resolve(quest_dict);
})
};
</script>

<script type="text/javascript">
Expand Down Expand Up @@ -407,7 +435,42 @@
mode_studied = data.intro.mode_studied
// Load list of plots corresponding to study/program
dynamic_labels = data.label_options
if (data.intro.program_or_study == 'program') {
surveys = data.survey_info.surveys
console.log(data.survey_info['trip-labels'])
if (data.survey_info['trip-labels'] === 'ENKETO') { //CASE: SURVEYS
survey_list = Object.keys(surveys)
survey_list = survey_list.filter(name => name !== 'UserProfileSurvey')

sheet_list = []
for (name in survey_list) {
form_path = data.survey_info.surveys[survey_list[name]].formPath;
//hard code the old survey
// form_path = 'https://raw.githubusercontent.com/e-mission/nrel-openpath-deploy-configs/main/survey_resources/dfc-fermata/fermata-ev-return-trip-v0.xml';
//THIS ASSUMES THE FILENAME IS THE SAME AS THE FORM PATH BUT WITH xml FILE TYPE
l_path = form_path.split('.')
l_path.splice(l_path.length -1, 1, 'xml');
console.log(l_path);
sheet_path = l_path.join('.')
sheet_list.push(sheet_path)
}

getDictionaryList(sheet_list).then((quest_dict) => {
console.log(quest_dict);
load_file = "metrics_study_surveys.html"
$.get(load_file, function (file) {
Object.entries(quest_dict).forEach(([key, value]) => {
var text = '<option ' + 'value="' + key + '" data-sizex="4" data-sizey="4">' + value + '</option>';
file = file.concat('\n', text);
});
console.log("configuring units");
const unitConfigured = file.replaceAll("${data.display_config.use_imperial}", dist_units);
$('#metric').append(unitConfigured);
addPreconfiguredMetrics(Object.keys(quest_dict).slice(0, 5)); //only adding the first 6 elements
});
});

}
else if (data.intro.program_or_study == 'program') { //CASE: PROGRAM
// Note: We're disabling energy metrics on public dashboard when dynamic labels are available.
// TODO: Remove the if (data.label_options) in future when energy computation is handled properly.
if (dynamic_labels) {
Expand Down
13 changes: 13 additions & 0 deletions frontend/metrics_study_surveys.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!-- htmnl options should be 1 per chart question -->
<!-- <option value="question key" data-sizex="4" data-sizey="4">translated question</option> -->

<option value="ntrips_sensed_mode" data-sizex="4" data-sizey="4">Number of trips (sensed)</option>
<option value="ntrips_under10miles_sensed_mode" data-sizex="4" data-sizey="4">Trip count under 80th Percentile (sensed)</option>
<option value="miles_sensed_mode" data-sizex="4" data-sizey="4">Trip distance (${data.display_config.use_imperial}) by mode (sensed)</option>
<option value="miles_sensed_mode_land" data-sizex="4" data-sizey="4">Trip distance by land mode (sensed)</option>
<option value="average_miles_sensed_mode" data-sizex="6" data-sizey="4">Average trip length (${data.display_config.use_imperial}) (sensed)</option>
<option value="ntrips_per_day" data-sizex="6" data-sizey="4">Trip frequency</option>
<option value="ntrips_sensed_per_day" data-sizex="6" data-sizey="4">Trip frequency (sensed)</option>
<option value="ntrips_per_weekday" data-sizex="6" data-sizey="4">Trip frequency (weekday)</option>
<option value="ntrips_sensed_per_weekday" data-sizex="6" data-sizey="4">Trip frequency (weekday, sensed)</option>
<option value="ts_users" data-sizex="8" data-sizey="2">Timeseries of active users</option>
3 changes: 2 additions & 1 deletion viz_scripts/bin/generate_plots.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,8 @@ def compute_for_date(month, year):
include_test_users=dynamic_config.get('metrics', {}).get('include_test_users', False),
dynamic_labels = dynamic_labels,
use_imperial = dynamic_config.get('display_config', {}).get('use_imperial', True),
sensed_algo_prefix=dynamic_config.get('metrics', {}).get('sensed_algo_prefix', "cleaned"))
sensed_algo_prefix=dynamic_config.get('metrics', {}).get('sensed_algo_prefix', "cleaned"),
survey_info = dynamic_config.get('survey_info', {}))

print(f"Running at {arrow.get()} with params {params}")

Expand Down
1 change: 1 addition & 0 deletions viz_scripts/docker/crontab
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@
0 8 * * * python bin/generate_plots.py mode_specific_metrics.ipynb default >> /var/log/intake.stdinout 2>&1
0 8 * * * python bin/generate_plots.py mode_specific_timeseries.ipynb default >> /var/log/intake.stdinout 2>&1
0 8 * * * python bin/generate_plots.py energy_calculations.ipynb default >> /var/log/intake.stdinout 2>&1
0 8 * * * python bin/generate_plots.py survey_responses.ipynb default >> /var/log/intake.stdinout 2>&1
# For testing only
# */5 * * * * python bin/generate_plots.py mode_purpose_share.ipynb default >> /var/log/intake.stdinout 2>&1
2 changes: 2 additions & 0 deletions viz_scripts/docker/environment36.dashboard.additions.yml
shankari marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ channels:
- defaults
dependencies:
- seaborn=0.11.1
- git
- pip:
- nbparameterise==0.6
- devcron==0.4
- git+https://github.com/JGreenlee/[email protected]
4 changes: 3 additions & 1 deletion viz_scripts/plots.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,15 @@ def merge_small_entries(labels, values):
# We could have already had a non-zero other, and it could be small or large
if "Other" not in v2l_df.index:
# zero other will end up with misc_count
v2l_df.loc["Other"] = misc_count
if misc_count.vals > 0:
v2l_df.loc["Other"] = misc_count
elif "Other" in small_chunk.index:
# non-zero small other will already be in misc_count
v2l_df.loc["Other"] = misc_count
else:
# non-zero large other, will not already be in misc_count
v2l_df.loc["Other"] = v2l_df.loc["Other"] + misc_count

disp.display(v2l_df)

return (v2l_df.index.to_list(),v2l_df.vals.to_list(), v2l_df.pct.to_list())
Expand Down
29 changes: 29 additions & 0 deletions viz_scripts/scaffolding.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,13 @@ def load_all_confirmed_trips(tq):
disp.display(all_ct.head())
return all_ct

def load_all_composite_trips(tq):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Abby-Wheelis why do you need to load composite trips? confirmed trips also have the user input handled properly and embedded in them.
https://github.com/e-mission/e-mission-server/blob/bc2d7b6b5f9fa1a9e981bbc5b911d7be9aaa393e/emission/core/wrapper/confirmedtrip.py#L24

Composite trips give you all the elements of the trip (the sections, stops and location points) in one object, but I don't think you need to use those elements for this PR.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we needed sections for a previous way that the survey choice was handled, but we should probably update them now, will the composite trips have the bluetooth sensed modes?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The latest filtering for the survey assignment uses confirmedMode?.baseMode == 'E_CAR', do you know where this confirmed mode comes from? I had been using the older version that relied on sections because that was what I had working (and therefore needed the composite trips). Is the bluetooth detected mode stored as a label?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it will be once e-mission/e-mission-server#965 is merged. In the interim, as a hack, we will need to store the confirmedMode in the public dashboard pre-processing by treating the BLE objects as user input.

Something like the following.

import emission.storage.decorations.trip_queries as esdt

def get_confirmed_mode(ts, trip, ble_vehicle_mapping):
     matching_beacon = esdt.get_user_input_for_timeline_entry(ts, trip, "background/bluetooth_ble")
     return ble_vehicle_mapping.get(matching_beacon, None)

you can call it either before converting the trip list to a dataframe or to fill in the dataframe column later (preferable) using something like df['confirmedMode'] = df.apply(lambda trip: get_confirmed_mode(ts, trip, ble_vehicle_mapping)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are ts and ble_vehicle_mapping? I'm getting errors about data being missing from the trips as well, but this seemed to work out when I wrapped the trip like {data: trip} because I could see that it was searching for the attribute data.start_ts. I tried the start_ts first as ts thinking it was timestamp, but I think it's actually timeseries, so I tried esta.TimeSeries.get_aggregate_time_series() but I'm only getting None back

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm still getting None for all of the entries when I look up the "background/bluetooth_ble", I see in the blame that this was added to the server about 3 weeks ago, which I'm imagining would have been right around the time we started the Alpha testing - and the data I have is from the 15th, which was chosen to have the data from when GSA was in town testing ... I was thinking maybe it wouldn't be in this data but I just talked myself out of that, I'll keep investigating I think this is something I must be doing and not the data

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was using the dump from the 24th and the entries are there. don't know about the 15th

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried loading the most recent data, still no luck getting any of the "background/bluetooth_ble" entries. I am able to run the line matching_beacon = esdt.get_user_input_for_trip("analysis/confirmed_trip", trip._id, trip.user_id, "background/bluetooth_ble") across all of the trips, but am getting None in response for all of them. I have also tried with "cleaned_trip" and "composite_trip" but still all None, am I using the right trip key?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have now loaded the new data with ble_sensed_summary -- though not many of the trips seem to have a detected mode - of the 990 trips I have, 330 have an 'UNKOWN' mode, and 6 are sensed 'CAR'

I used expanded_ct["confirmedMode?.baseMode"] = participant_ct_df.ble_sensed_summary.apply(lambda md: max(md["distance"], key=md["distance"].get)), and it seems to be working well, but not sure if it's what we expect, I think I'd expect more trips sensed as CAR, and some sensed as ECAR ... maybe I need to reevaluate how we distill the mode?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

poking around at the entries for ble_sensed_summary, I do see some where CAR appears but is not the max by distance, is this expected? Are we picking up the beacon for some (but not all) of the trip? Or are they just trips with multiple segments? Only 6 trips with a detected beacon just seems quite low to me

agg = esta.TimeSeries.get_aggregate_time_series()
all_ct = agg.get_data_df("analysis/composite_trip", tq)
print("Loaded all composite trips of length %s" % len(all_ct))
disp.display(all_ct.head())
return all_ct

def load_all_participant_trips(program, tq, load_test_users):
participant_list = get_participant_uuids(program, load_test_users)
all_ct = load_all_confirmed_trips(tq)
Expand All @@ -69,6 +76,16 @@ def load_all_participant_trips(program, tq, load_test_users):
disp.display(participant_ct_df.head())
return participant_ct_df

def filter_composite_trips(all_comp_trips, program, load_test_users):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that you have removed load_composite_trips, you can also remove filter_composite_trips

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see that filter_composite_trips is not identical to filter_labeled_trips because it doesn't filter by blank user input. However, I don't understand why you are not filtering by blank user input. It should be possible to just call load_all_participant_trips instead of load_all_confirmed_trips followed by filter_composite_trips to achieve the same result.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

However, I don't understand why you are not filtering by blank user input.

I do, later, but I need all of the trips in order to create the "all trips for which a survey was prompted" information

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made some changes to the data loading (including pushing it to scaffolding) in #135 - I have the filtered, unfiltered, and file suffix all returned from a function in scaffolding now - I don't have the debug_df or quality_text included since it varies chart to chart

participant_list = get_participant_uuids(program, load_test_users)
# CASE 1 of https://github.com/e-mission/em-public-dashboard/issues/69#issuecomment-1256835867
if len(all_comp_trips) == 0:
return all_comp_trips
participant_ct_df = all_comp_trips[all_comp_trips.user_id.isin(participant_list)]
print("After filtering, found %s participant trips " % len(participant_ct_df))
disp.display(participant_ct_df.head())
return participant_ct_df

def filter_labeled_trips(mixed_trip_df):
# CASE 1 of https://github.com/e-mission/em-public-dashboard/issues/69#issuecomment-1256835867
if len(mixed_trip_df) == 0:
Expand Down Expand Up @@ -214,6 +231,18 @@ def mapping_color_labels(dynamic_labels, dic_re, dic_pur):

return colors_mode, colors_purpose, colors_sensed

# Function: Maps survey answers to colors.
# Input: dictionary of raw and translated survey answers
# Output: Map for color with survey answers
def mapping_color_surveys(dic_options):
dictionary_values = (list(OrderedDict.fromkeys(dic_options.values())))

colors = {}
for i in range(len(dictionary_values)):
colors[dictionary_values[i]] = plt.cm.tab10.colors[i%10]

return colors

def load_viz_notebook_sensor_inference_data(year, month, program, include_test_users=False, sensed_algo_prefix="cleaned"):
""" Inputs:
year/month/program = parameters from the visualization notebook
Expand Down
Loading