Files
tubestats-presentation/index.html
T

414 lines
12 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>TubeStats</title>
<link rel="stylesheet" href="dist/reset.css">
<link rel="stylesheet" href="dist/reveal.css">
<link rel="stylesheet" href="dist/theme/simple.css" id="theme">
<!-- Theme used for syntax highlighted code -->
<link rel="stylesheet" href="plugin/highlight/monokai.css" id="highlight-theme">
</head>
<body>
<div class="reveal">
<div class="slides">
<section data-markdown>
# TubeStats
*A hobby project: Consistency in a YouTube channel*
Shivan Sivakumaran
</section>
<section data-markdown>
## Inspiration
- Ali Abdaal
- Consistency - how consistent?
- Getting better as a beginner
- www.tubestats.app
</section>
<section data-markdown>
## What does TubeStats do?
1. Takes user input
2. Provides statistics
</section>
<section data-background-image="tubestats_parsing.gif"
data-background-size="750px">
</section>
<section data-markdown>
## 1. User input
```python
# Channel ID
'UCoOae5nYA7VqaXzerajD0lg'
# Link to channel
'https://www.youtube.com/channel/UCoOae5nYA7VqaXzerajD0lg'
# Link to video
'https://www.youtube.com/watch?v=epF2SYpWtos'
# Video ID
'epF2SYpWtos'
```
</section>
<section data-markdown>
## 2. Statistics
![](all-graph.png)
</section>
<section data-markdown>
![](time-diff.png)
</section>
<section data-markdown>
## How does TubeStats work?
### Part 1 of 2
1. How to set up a development environment?
2. How to access the video information?
3. How to store password and API keys?
4. How do we get and store the video statistics?
</section>
<section data-markdown>
## 1. Development environment
```bash
$ mkdir tubestats
$ cd tubestats
$ python3 -m venv venv
$ source venv/bin/activate
$ (venv)
$ git init
```
</section>
<section data-markdown>
## 2. Video information
- use `beautifulsoup`, `scraPY`, `selenium`?
- YouTube Data API
- `google-api-python-client`
</section>
<section data-markdown>
## 3. Storing API Keys
- Hard code?
- `python-dotenv`
- `.env`
- `.gitignore`
</section>
<section data-markdown>
### 3. Storing API keys
```
# .env
API_KEY=xxxxxxxx
```
```python
# tubestats/youtube_api.py
from dotenv import load_dotenv
load_dotenv()
API_KEY = os.getenv('API_KEY')
```
</section>
<section data-markdown>
## 4. Get YouTube video statistics
- Access API
- Channel upload playlist
- Video statistics
- `pandas` dataframe
</section>
<section data-markdown>
### 4. Get YouTube video statistics
```python
import googleapiclient.discovery
load_dotenv()
api_service_name = 'youtube'
api_version = 'v3'
youtube = googleapiclient.discovery.build(
api_service_name,
api_version,
developerKey=os.getenv('API_KEY'))
```
</section>
<section data-markdown>
```python [|3|5-16|17-18|20-29|30-32]
# tubestates/youtube_api.py
upload_playlist_ID = channel_data['upload_playlist_ID']
video_response = []
next_page_token = None
while True:
# obtaining video ID + titles
playlist_request = youtube.playlistItems().list(
part='snippet,contentDetails',
maxResults=50, # API Limit is 50
pageToken=next_page_token,
playlistId=upload_playlist_ID,
)
playlist_response = playlist_request.execute()
# isolating video ID
vid_subset = [ vid_ID['contentDetails']['videoId']
for vid_ID in playlist_response['items'] ]
# retrieving video statistics
vid_info_subset_request = youtube.videos().list(
part='snippet,contentDetails,statistics',
id=vid_subset
)
vid_info_subset_response = vid_info_subset_request.execute()
video_response.append(vid_info_subset_response)
# obtaining page token
next_page_token = playlist_response.get('nextPageToken') # get method used because token may not exist
if next_page_token is None:
break
df = pd.json_normalize(video_response, 'items')
return df
</section>
<section data-markdown>
### Video statistics
![](dataframe.png)
</section>
<section data-markdown>
## How does TubeStats work?
### Part 2 of 2
5. How to organise the code?
6. How to test the code?
7. How to display the data and allow interaction?
8. How to account for variable input?
</section>
<section data-markdown>
## 5. Organising code
```bash [1-9|10-15|16-20|21]
tubestats/
├── data
│   ├── channel_data.pkl
│   └── video_data.pkl
├── LICENSE
├── Procfile
├── README.MD
├── requirements.txt
├── setup.sh
├── tests
│   ├── __init__.py
│   ├── test_settings.py
│   ├── test_youtube_api.py
│   ├── test_youtube_data.py
│   └── test_youtube_parser.py
├── tubestats
│   ├── __init__.py
│   ├── youtube_api.py
│   ├── youtube_data.py
│   └── youtube_parser.py
└── youtube_presenter.py
</section>
<section data-markdown>
## 6. Testing
```python [|15-20]
# tests/tests_youtube_api.py
from tubestats.youtube_api import create_api, YouTubeAPI
from tests.test_settings import set_channel_ID_test_case
from pathlib import Path
import pytest
import googleapiclient
import pandas
def test_create_api():
youtube = create_api()
assert isinstance(youtube, googleapiclient.discovery.Resource)
@pytest.fixture()
def youtubeapi():
channel_ID = set_channel_ID_test_case()
yt = YouTubeAPI(channel_ID)
return yt
def test_get_video_data(youtubeapi):
df = youtubeapi.get_video_data()
assert isinstance(df, pandas.core.frame.DataFrame)
# saving video data to save API calls for later testing
BASE_DIR = Path(__file__).parent.parent
df.to_pickle(BASE_DIR / 'data' / 'video_data.pkl')
</section>
<section data-markdown>
## 7. Sharing to the world
- graphs with tool tips, `altair`
- creating interaction with `streamlit`
- hosting on Heroku
</section>
<section data-markdown>
### 7. Sharing to the world
```python []
# tubestats/youtube_data.py
import altair as alt
def scatter_all_videos(self, df: pd.core.frame.DataFrame) -> alt.vegalite.v4.Chart:
df_views = df
c = alt.Chart(df_views, title='Plot of videos over time').mark_point().encode(
x=alt.X('snippet\.publishedAt_REFORMATED:T', axis=alt.Axis(title='Date Published'), scale=alt.Scale(type='log')),
y=alt.Y('statistics\.viewCount:Q', axis=alt.Axis(title='View Count')),
color=alt.Color('statistics\.like-dislike-ratio:Q', scale=alt.Scale(scheme='turbo'), legend=None),
tooltip=['snippet\.title:N', 'statistics\.viewCount:Q', 'statistics\.like-dislike-ratio:Q'],
size=alt.Size('statistics\.viewCount:Q', legend=None)
)
return c
</section>
<section data-markdown>
### 7. Sharing to the world
```python [|5-14|16-19]
# youtube_presenter.py
import streamlit as st
def date_slider(date_end=datetime.today()):
date_start, date_end = st.slider(
'Select date range to include:',
min_value=first_video_date, # first video
max_value=last_video_date, #value for date_end
value=(first_video_date , last_video_date), #same as min value
step=timedelta(days=2),
format='YYYY-MM-DD',
key=999)
return date_start, date_end
date_start, date_end = date_slider()
transformed_df = youtuber_data.transform_dataframe(date_start=date_start, date_end=date_end)
c = youtuber_data.scatter_all_videos(transformed_df)
st.altair_chart(c, use_container_width=True)
</section>
<section data-markdown>
### 7. Sharing with the world
```shell []
$ streamlit run youtube_presenter.py
You can now view your Streamlit app in your browser.
Local URL: http://localhost:8501
Network URL: http://192.0.0.0.1
</section>
<section data-markdown>
![](all-graph.png)
</section>
<section data-markdown>
### 7. Sharing to the world
```bash
$ (venv) pip freeze > requirements.txt
```
```bash
# setup.sh
mkdir -p ~/.streamlit/echo "\
[server]\n\
headless = true\n\
port = $PORT\n\
enableCORS = false\n\
\n\
" > ~/.streamlit/config.toml
```
```bash
# Procfile
web: sh setup.sh && streamlit run youtube_presenter.py
```
```bash
$ heroku login
$ heroku create tubestats
$ git push heroku main
```
</section>
<section data-markdown>
## 8. Different user input
- taking video ID
- URL links
- using regex, `re` module
</section>
<section data-markdown>
### 8. Different user input
```python []
import re
LINK_MATCH = r'(^.*youtu)(\.be|be\.com)(\/watch\?v\=|\/)([a-zA-Z0-9_-]+)(\/)?([a-zA-Z0-9_-]+)?'
m = re.search(LINK_MATCH, for_parse)
video_id = m.group(4) # video ID
if video_id == 'channel':
return m.group(6) # Channel ID
elif video_id == 'user':
channel_username = m.group(6) # Channel Username
</section>
<section data-markdown>
### 8. Different user input
![](regex.png)
</section>
<section data-markdown>
## Things to consider for the future
- Error handling
- Maxing API calls
- Comparing different channels
- DataFrame and memory
</section>
<section data-markdown>
### DataFrame immutability and memory?
```python []
df = self.df
df = df[['snippet.publishedAt',
'snippet.title',
...
'statistics.favoriteCount',
'statistics.commentCount']]
df = df.fillna(0)
df = df.astype({'statistics.viewCount': 'int',
...
'statistics.commentCount': 'int',})
df['statistics.viewCount_NLOG'] = df['statistics.viewCount'].apply(lambda x : np.log(x))
df = df.sort_values(by='snippet.publishedAt_REFORMATED', ascending=True)
</section>
<section data-markdown>
## What did I learn
- Project based learning
- 'minimal viable product'
</section>
<section data-markdown>
## Conclusion
- Analysing consistency
- YouTube Data API --> pandas --> altair --> Heroku
- Share your work!
</section>
<section data-markdown>
## Acknowledgements
- Menno Finlay-Smits
</div>
</div>
<script src="dist/reveal.js"></script>
<script src="plugin/notes/notes.js"></script>
<script src="plugin/markdown/markdown.js"></script>
<script src="plugin/highlight/highlight.js"></script>
<script>
// More info about initialization & config:
// - https://revealjs.com/initialization/
// - https://revealjs.com/config/
Reveal.initialize({
hash: true,
// Learn about plugins: https://revealjs.com/plugins/
plugins: [ RevealMarkdown, RevealHighlight, RevealNotes ]
});
</script>
</body>
</html>