DIY Interactive App just using Python
By: Anshul Sharma
Tags: Python, Visualization, Kaggle, App Development, Data Science

Photo by Stephen Dawson on Unsplash

Make an interactive app just using python and showcase it through Jupyter/Kaggle/Colab……. Notebooks.

A Sample of the app

Motivation

Doing visualizations on a huge dataset with a lot of categories can be a tedious task. You always end up showing just the best or the worst as showing details about let’s say 200 different categories is not feasible. So, what could be better than creating an app through which you can show all the required details at once, save on your time, and also people’s time reading your notebook.

Through this story, I will be providing all the steps on how to create an app just using python. The only prerequisite is good knowledge of python and a little bit of knowledge of the pandas library(only because I’m using it in this project). I’ll be using ipywidgets library also but knowledge of it is optional as I also worked with it the first time.

About the App

I call it the Birdcall App. What is does is show an image, description, stats, waveform of the chirp, and the location where the chirp of the bird was recorded. One can scroll through the species of the birds, the recordist who recorded the chirp, and the audio file. The contents of the app change depending on the selection. The same way as you can see in the top gif.

The dataset I have used is available here. It’s a part of an ongoing kaggle competition. The data contains all the details about the birds chirp. The only thing it doesn’t provide is the bird description and the image of the bird. So, that’s the extra bit that needs to be scrapped.

Gathering the Data

For the app to work we need the data. As mentioned above we have all the data to create our app. Just for the images and description of the bird species, we’ll have to scrape the web.

Here’s the code to get the bird details such as description and images. I have scrapped these details using beautiful soup and requests library. The webpage from where I have scraped the data is https://ebird.org/species.

dic_bird = {}

for code in df_train.ebird_code.unique():

if code not in dic_bird:

url = 'https://ebird.org/species/'+code

r = requests.get(url)

soup = BeautifulSoup(r.content, 'html.parser')

desc = soup.find('meta',property="og:description")

image = soup.find('meta',property="og:image")

dic_bird[code]={}

dic_bird[code]['description'] = desc['content']

dic_bird[code]['image'] = image['content'] #Convert the dictionary to dataframe.

df = pd.DataFrame(columns=['species','description','image'])

i = 0

for key,val in dic_bird.items():

df.loc[i,'species'] = key

df.loc[i,'description'] = val['description']

df.loc[i,'image'] = val['image']

i+=1

df.to_csv('bird_details.csv',index=False)

As can be seen from the code, to get the details of a particular species of the bird we need to append its `ebird_code` to the site URL.

Designing the Components

After collecting all the data, the next logical step is to create the components of the app one by one. I’ll be listing down the steps for all components one by one in sequence.

1. Species, Recordist & Audio options

Example of how the scrolling options look in the app

The user can easily scroll through all the options easily. Also, the values of recordists and filename change on the basis of the species selected. This will be covered in the interactions section later. The code to implement this is as follows:

I have used a dictionary to add initial values and created separate methods to get the data. You can write your own.

import ipywidgets as widgets species = widgets.Dropdown(

description = 'Species: ',

value = default_values['species'],

options = df_train['species'].unique().tolist(),

layout=dict(width='233px')

) recordists = widgets.Dropdown(

description = 'Recordist: ',

value = default_values['recordist'],

options = get_recordist_options(default_values['species']),

layout=dict(width='233px')

) audios = widgets.Dropdown(

description = 'Filename: ',

value = default_values['audio'],

options = get_audio_options(default_values['species'],default_values['recordist']),

layout=dict(width='233px')

)

container1 = widgets.HBox(children=[species, recordists, audios])

2. Title + Audio Container

The container with the title and option to play the mp3 file

The title can be easily added using the HTML widgets. It changes depending on the species selected. The audio is shown using the Audio widget.

title = widgets.HTML('<h1>{}</h1>'.format(default_values['species']),layout=dict(width='350px'))

out = widgets.Output(layout=dict(width='350px',margin='10px 0px 0px 0px'))

out.append_display_data(ipd.Audio(default_values['filename']))

container2 = widgets.HBox(children=[title,out])

3. Image + Description Container

Container showing Bird Image and Description

I have added image and description using the HTML widget. Later, both are pooled into a Horizontal Box container using HBox widget. Also, layout can be defined for each widget separately. This is exactly similar to CSS.

im = widgets.HTML('<img src="{}"/>'.format(default_values['image']),

layout=dict(height='250px',width='300px'))

text = widgets.HTML('<h5>{}</h5>'.format(default_values['text']),

layout=dict(height='250px',width='400px',margin='10px 0px 0px 10px'))

container3 = widgets.HBox(children=[im,text])

4. Bird Stats

Bird Statistics

Avg. Rating is the mean rating of all the ratings for short recordings of bird’s species calls generously uploaded by users of xenocanto.org. Average duration is the mean duration of the bird calls. Average elevation as the name suggests is the mean height the bird travels at.

r,d,e = get_stats(default_values['species'])

rating = widgets.HTML('<h2>Avg. Rating</h2><h4>{}</h4>'.format(r),layout=dict(width='233px'))

duration = widgets.HTML('<h2>Avg. Duration</h2><h4>{} s</h4>'.format(d),layout=dict(width='233px'))

elevation = widgets.HTML('<h2>Avg. Elevation</h2><h4>{} m</h4>'.format(e),layout=dict(width='233px'))

container4 = widgets.HBox(children=[rating,duration,elevation])

5. Waveform Plot

Birdcall waveform plotted using Plotly

I have used a helper function to convert mp3 to NumPy array and then used Plolty FigureWidget to create the waveform plot. Simple matplotlib plots can be used as well but I always prefer Plotly owing to its added interactivity. The waveform changes on the basis of the audio file selected.

rate, arr = read(default_values['filename'])

title_g = widgets.HTML('<h2>Waveform of the bird chirp</h2>')

g = plot_waveform(arr,default_values['filename'].split('/')[-1])

6. Map Plot

Map showing the recording location

I have used Plotly’s scattermapbox to plot the recording location on the map. It also changes depending on the audio file selected.

title_map = widgets.HTML('<h2>Where the recording was made?</h2>')

loc_map = plot_location(default_values['latitude'],default_values['longitude'],default_values['location'])

Please note all the plotly plots are ultimately added to the plotly.graph_objects.Figurewidget for ipywidgets to work properly with them.

7. Combining all components

After designing all the components, just pool them into a single component.

widgets.VBox(children=[container1,container2,container3,container4,title_g,g,title_map,loc_map])

App Interactions

Nobody likes a static app! So, to add interactions to our ipywidgets app we need to add callback functions which will take care of changing the data of the components we designed.

The following are the interactions which take place in my app:

change species -> change recordists dropdown, change audios dropdown, change title, image & text, and change bird stats.

-> change recordists dropdown, change audios dropdown, change title, image & text, and change bird stats. change recordist -> change audios dropdown.

-> change audios dropdown. change audio-> change waveform and recording location map.

I have kept separate callbacks to stick to the flow listed above. Please note when response_sp() callback is triggered it will automatically trigger response_re() and response_au() callbacks respectively as recordists and audio values are changing in the species change callback function.

Similarly, response_re() will trigger response_au(). And response_au() alone won’t trigger any callbacks.

def response_sp(change):

ecode = get_ebird_code(species.value)



#change recordists dropdown

options = get_recordist_options(species.value)

recordists.options = options

recordists.value = options[0]



#change filename dropdown

options = get_audio_options(species.value,recordists.value)

audios.options = options

audios.value = options[0]



#change title, image and text

title.value = '<h1>{}</h1>'.format(species.value)

im.value = '<img src="{}"/>'.format(get_image(ecode))

text.value = '<h5>{}</h5>'.format(get_text(ecode))



#change bird stats

r,d,e = get_stats(species.value)

rating.value = '<h2>Avg. Rating</h2><h4>{}</h4>'.format(r)

duration.value = '<h2>Avg. Duration</h2><h4>{} s</h4>'.format(d)

elevation.value = '<h2>Avg. Elevation</h2><h4>{} m</h4>'.format(e) def response_re(change):

#change audios dropdown

options = get_audio_options(species.value,recordists.value)

audios.options = options

audios.value = options[0] def response_au(change):

ecode = get_ebird_code(species.value)

file = audios.value

filename = get_filename(ecode,file)

with out:

ipd.clear_output()

ipd.display(ipd.Audio(filename))

rate, arr = read(filename)

g_new = plot_waveform(arr,filename.split('/')[-1])

g.update(data=g_new.data,layout=g_new.layout)

lat = df_train.loc[df_train['filename']==audios.value,'latitude'].tolist()[0]

lon = df_train.loc[df_train['filename']==audios.value,'longitude'].tolist()[0]

location = df_train.loc[df_train['filename']==audios.value,'location'].tolist()[0]

loc_map_new = plot_location(float(lat),float(lon),location)

loc_map.update(data=loc_map_new.data,layout=loc_map_new.layout) #Callback functions

species.observe(response_sp, names="value")

recordists.observe(response_re, names="value")

audios.observe(response_au, names="value")

The last three lines of the code show how to declare the callback functions for a widget.

Finally, our app is complete!!

To access the full code of the app click here. To see more projects on which I have worked click here.

I hope I have explained it in a clear manner. In case you have any doubts or have any feedback do leave a comment. Thanks for reading this far :)