Files
Step_Data_Project_India/eda.ipynb
T

1.6 MiB

In [2]:
%pip install seaborn
Defaulting to user installation because normal site-packages is not writeable
Requirement already satisfied: seaborn in C:\Users\PMLS\AppData\Roaming\Python\Python314\site-packages (0.13.2)
Requirement already satisfied: numpy!=1.24.0,>=1.20 in C:\Users\PMLS\AppData\Roaming\Python\Python314\site-packages (from seaborn) (2.4.4)
Requirement already satisfied: pandas>=1.2 in C:\Users\PMLS\AppData\Roaming\Python\Python314\site-packages (from seaborn) (3.0.2)
Requirement already satisfied: matplotlib!=3.6.1,>=3.4 in C:\Users\PMLS\AppData\Roaming\Python\Python314\site-packages (from seaborn) (3.10.8)
Requirement already satisfied: contourpy>=1.0.1 in C:\Users\PMLS\AppData\Roaming\Python\Python314\site-packages (from matplotlib!=3.6.1,>=3.4->seaborn) (1.3.3)
Requirement already satisfied: cycler>=0.10 in C:\Users\PMLS\AppData\Roaming\Python\Python314\site-packages (from matplotlib!=3.6.1,>=3.4->seaborn) (0.12.1)
Requirement already satisfied: fonttools>=4.22.0 in C:\Users\PMLS\AppData\Roaming\Python\Python314\site-packages (from matplotlib!=3.6.1,>=3.4->seaborn) (4.62.1)
Requirement already satisfied: kiwisolver>=1.3.1 in C:\Users\PMLS\AppData\Roaming\Python\Python314\site-packages (from matplotlib!=3.6.1,>=3.4->seaborn) (1.5.0)
Requirement already satisfied: packaging>=20.0 in C:\Users\PMLS\AppData\Roaming\Python\Python314\site-packages (from matplotlib!=3.6.1,>=3.4->seaborn) (26.1)
Requirement already satisfied: pillow>=8 in C:\Users\PMLS\AppData\Roaming\Python\Python314\site-packages (from matplotlib!=3.6.1,>=3.4->seaborn) (12.2.0)
Requirement already satisfied: pyparsing>=3 in C:\Users\PMLS\AppData\Roaming\Python\Python314\site-packages (from matplotlib!=3.6.1,>=3.4->seaborn) (3.3.2)
Requirement already satisfied: python-dateutil>=2.7 in C:\Users\PMLS\AppData\Roaming\Python\Python314\site-packages (from matplotlib!=3.6.1,>=3.4->seaborn) (2.9.0.post0)
Requirement already satisfied: tzdata in C:\Users\PMLS\AppData\Roaming\Python\Python314\site-packages (from pandas>=1.2->seaborn) (2026.2)
Requirement already satisfied: six>=1.5 in C:\Users\PMLS\AppData\Roaming\Python\Python314\site-packages (from python-dateutil>=2.7->matplotlib!=3.6.1,>=3.4->seaborn) (1.17.0)
Note: you may need to restart the kernel to use updated packages.
[notice] A new release of pip is available: 26.1.2 -> 26.2.1
[notice] To update, run: python.exe -m pip install --upgrade pip
In [3]:
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns

sns.set_theme(style="whitegrid")

# The already-cleaned dataset your main.py uses
df = pd.read_json('Datasets/hours.json')
print(df.shape)
df.head()
Out [3]:
(49798, 35)
Date Year Month Hour_0 Hour_1 Hour_2 Hour_3 Hour_4 Hour_5 Hour_6 ... Hour_22 Hour_23 DayOfWeek_Friday DayOfWeek_Monday DayOfWeek_Saturday DayOfWeek_Sunday DayOfWeek_Thursday DayOfWeek_Tuesday DayOfWeek_Wednesday user
0 2015-09-16 2015 9 0 0 0 0 0 0 0 ... 38 0 0 0 0 0 0 0 1 0
1 2015-09-17 2015 9 0 0 0 0 0 0 0 ... 103 0 0 0 0 0 1 0 0 0
2 2015-09-18 2015 9 0 0 0 0 0 0 180 ... 782 48 1 0 0 0 0 0 0 0
3 2015-09-19 2015 9 0 0 0 0 0 0 0 ... 359 562 0 0 1 0 0 0 0 0
4 2015-09-20 2015 9 165 0 0 0 0 0 0 ... 0 0 0 0 0 1 0 0 0 0

5 rows × 35 columns

In [4]:
# How many missing values per column?
missing_counts = df.isnull().sum().sort_values(ascending=False)
print(missing_counts[missing_counts > 0])

# Visualize where the gaps are
plt.figure(figsize=(14, 6))
sns.heatmap(df.isnull(), cbar=False, cmap='viridis')
plt.title('Missing values map (yellow = missing)')
plt.show()
Series([], dtype: int64)
In [5]:
user_counts = df['user'].value_counts().sort_index()

plt.figure(figsize=(14, 5))
user_counts.plot(kind='bar')
plt.title('Number of day-records per user')
plt.xlabel('User ID')
plt.ylabel('Number of days of data')
plt.show()

print('Fewest days:', user_counts.min(), '| Most days:', user_counts.max())
print('Average days per user:', user_counts.mean().round(1))
Fewest days: 3 | Most days: 2102
Average days per user: 976.4
In [6]:
hour_cols = [c for c in df.columns if c.startswith('Hour_')]

plt.figure(figsize=(16, 6))
sns.boxplot(data=df[hour_cols])
plt.title('Step count distribution by hour of day (outliers = dots)')
plt.xticks(rotation=45)
plt.show()
In [7]:
# Check if group info exists in the processed data
print(df.columns.tolist())
['Date', 'Year', 'Month', 'Hour_0', 'Hour_1', 'Hour_2', 'Hour_3', 'Hour_4', 'Hour_5', 'Hour_6', 'Hour_7', 'Hour_8', 'Hour_9', 'Hour_10', 'Hour_11', 'Hour_12', 'Hour_13', 'Hour_14', 'Hour_15', 'Hour_16', 'Hour_17', 'Hour_18', 'Hour_19', 'Hour_20', 'Hour_21', 'Hour_22', 'Hour_23', 'DayOfWeek_Friday', 'DayOfWeek_Monday', 'DayOfWeek_Saturday', 'DayOfWeek_Sunday', 'DayOfWeek_Thursday', 'DayOfWeek_Tuesday', 'DayOfWeek_Wednesday', 'user']
In [8]:
plt.figure(figsize=(14, 12))
sns.heatmap(df[hour_cols].corr(), cmap='coolwarm', center=0)
plt.title('Correlation between hours of the day')
plt.show()
In [ ]:
In [9]:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from scipy import stats

df = pd.read_json('Datasets/hours.json')
hour_cols = [c for c in df.columns if c.startswith('Hour_')]

# Missingness correlation matrix — do certain hours go missing TOGETHER?
missing_matrix = df[hour_cols].isnull().astype(int)
plt.figure(figsize=(12, 10))
sns.heatmap(missing_matrix.corr(), cmap='coolwarm', center=0, annot=False)
plt.title('Do missing hours cluster together? (missingness correlation)')
plt.show()

# Is missingness related to WHICH user it is? (MAR test)
missing_by_user = missing_matrix.groupby(df['user']).mean().mean(axis=1)
print("Missingness rate varies by user — std dev:", missing_by_user.std().round(4))
missing_by_user.sort_values(ascending=False).head(10)
Out [9]:
C:\Users\PMLS\AppData\Roaming\Python\Python314\site-packages\seaborn\matrix.py:202: RuntimeWarning: All-NaN slice encountered
  vmin = np.nanmin(calc_data)
C:\Users\PMLS\AppData\Roaming\Python\Python314\site-packages\seaborn\matrix.py:207: RuntimeWarning: All-NaN slice encountered
  vmax = np.nanmax(calc_data)
Missingness rate varies by user — std dev: 0.0
user
0    0.0
1    0.0
2    0.0
3    0.0
4    0.0
5    0.0
6    0.0
7    0.0
8    0.0
9    0.0
dtype: float64
In [10]:
quality = pd.DataFrame({
    'n_days': df.groupby('user').size(),
    'missing_pct': missing_matrix.groupby(df['user']).mean().mean(axis=1) * 100,
    'zero_days_pct': (df.groupby('user')[hour_cols].sum(axis=1).groupby(df['user']).apply(lambda x: (x == 0).mean()) * 100) if False else None
})
# zero_days_pct done separately to avoid nested groupby issue:
daily_total = df[hour_cols].sum(axis=1)
zero_flag = (daily_total == 0)
quality['zero_days_pct'] = zero_flag.groupby(df['user']).mean() * 100
quality['quality_score'] = 100 - quality['missing_pct'] - quality['zero_days_pct']
quality.sort_values('quality_score').head(10)
Out [10]:
n_days missing_pct zero_days_pct quality_score
user
0 2070 0.0 0.0 100.0
1 2102 0.0 0.0 100.0
2 49 0.0 0.0 100.0
3 1005 0.0 0.0 100.0
4 510 0.0 0.0 100.0
5 1025 0.0 0.0 100.0
6 1713 0.0 0.0 100.0
7 874 0.0 0.0 100.0
8 954 0.0 0.0 100.0
9 3 0.0 0.0 100.0
In [11]:
user_counts = df['user'].value_counts()

# Shannon entropy — measures how "balanced" the class distribution is
# Max entropy = perfectly balanced. Big gap = severe imbalance.
probs = user_counts / user_counts.sum()
entropy = -np.sum(probs * np.log2(probs))
max_entropy = np.log2(len(user_counts))
print(f"Entropy: {entropy:.3f} bits (max possible: {max_entropy:.3f} bits)")
print(f"Balance ratio: {entropy/max_entropy:.2%}  (100% = perfectly balanced)")

# Imbalance ratio: biggest class vs smallest class
print(f"Imbalance ratio (max/min): {user_counts.max()/user_counts.min():.1f}x")

# Gini coefficient of the class distribution (borrowed from economics — inequality measure)
sorted_counts = np.sort(user_counts.values)
n = len(sorted_counts)
cum = np.cumsum(sorted_counts)
gini = (2 * np.sum((np.arange(1, n+1)) * sorted_counts) - (n+1) * cum[-1]) / (n * cum[-1])
print(f"Gini coefficient of class sizes: {gini:.3f} (0=equal, 1=max inequality)")
Entropy: 5.433 bits (max possible: 5.672 bits)
Balance ratio: 95.79%  (100% = perfectly balanced)
Imbalance ratio (max/min): 700.7x
Gini coefficient of class sizes: 0.310 (0=equal, 1=max inequality)
In [19]:
from scipy.stats import f_oneway, kruskal

# For each hour, test: does step count differ significantly ACROSS users?
# (This tells you which features are actually discriminative — worth feeding the model)
anova_results = []
for h in hour_cols:
    groups = [df[df['user']==u][h].dropna() for u in df['user'].unique()]
    groups = [g for g in groups if len(g) > 1]
    f_stat, p_val = f_oneway(*groups)
    anova_results.append({'hour': h, 'f_stat': f_stat, 'p_value': p_val})

anova_df = pd.DataFrame(anova_results).sort_values('f_stat', ascending=False)
print(anova_df.head(10))  # Top 10 most discriminative hours

# Show the most discriminative hours
top_hours = anova_df.head(10).sort_values('f_stat', ascending=True)

plt.figure(figsize=(10, 6))

plt.barh(
    top_hours['hour'],
    top_hours['f_stat']
)

plt.xlabel('ANOVA F-statistic')
plt.ylabel('Hour')
plt.title('Most discriminative hours between users')

plt.show()

print("Top 10 discriminative hours:")
print(anova_df.head(10)[['hour', 'f_stat', 'p_value']])
       hour     f_stat  p_value
5    Hour_5  72.617907      0.0
6    Hour_6  72.241065      0.0
8    Hour_8  61.546298      0.0
20  Hour_20  55.639084      0.0
1    Hour_1  47.432846      0.0
21  Hour_21  44.906896      0.0
19  Hour_19  42.048390      0.0
12  Hour_12  40.109184      0.0
4    Hour_4  37.500643      0.0
10  Hour_10  37.159183      0.0
Top 10 discriminative hours:
       hour     f_stat  p_value
5    Hour_5  72.617907      0.0
6    Hour_6  72.241065      0.0
8    Hour_8  61.546298      0.0
20  Hour_20  55.639084      0.0
1    Hour_1  47.432846      0.0
21  Hour_21  44.906896      0.0
19  Hour_19  42.048390      0.0
12  Hour_12  40.109184      0.0
4    Hour_4  37.500643      0.0
10  Hour_10  37.159183      0.0
In [13]:
from sklearn.decomposition import PCA
from sklearn.manifold import TSNE
from sklearn.preprocessing import StandardScaler

# Use a subset of users for a readable plot
top_users = df['user'].value_counts().head(10).index
subset = df[df['user'].isin(top_users)].dropna(subset=hour_cols)

X = StandardScaler().fit_transform(subset[hour_cols])
y = subset['user']

# PCA — linear separability
pca = PCA(n_components=2)
X_pca = pca.fit_transform(X)
print(f"Variance explained by 2 PCs: {pca.explained_variance_ratio_.sum():.1%}")

plt.figure(figsize=(10, 8))
sns.scatterplot(x=X_pca[:,0], y=X_pca[:,1], hue=y.astype(str), palette='tab10', alpha=0.6)
plt.title('PCA: Are users linearly separable in step-pattern space?')
plt.show()

# t-SNE — non-linear separability (closer to what a neural net "sees")
tsne = TSNE(n_components=2, perplexity=30, random_state=17)
X_tsne = tsne.fit_transform(X)

plt.figure(figsize=(10, 8))
sns.scatterplot(x=X_tsne[:,0], y=X_tsne[:,1], hue=y.astype(str), palette='tab10', alpha=0.6)
plt.title('t-SNE: Non-linear separability of users')
plt.show()
Variance explained by 2 PCs: 18.0%
In [14]:
from pandas.plotting import autocorrelation_plot

# Pick one well-populated user, look at their daily total step pattern over time
sample_user = quality.sort_values('n_days', ascending=False).index[0]
user_df = df[df['user'] == sample_user].copy()
daily_total = user_df[hour_cols].sum(axis=1)

plt.figure(figsize=(12, 5))
autocorrelation_plot(daily_total)
plt.title(f'Autocorrelation of daily steps — User {sample_user}')
plt.show()

# Day-of-week effect — is there a weekly rhythm?
if 'weekend' in df.columns:
    plt.figure(figsize=(8, 5))
    sns.boxplot(x=df['weekend'], y=df[hour_cols].sum(axis=1))
    plt.title('Total daily steps: weekday vs weekend')
    plt.show()
In [15]:
from sklearn.ensemble import IsolationForest

iso = IsolationForest(contamination=0.02, random_state=17)
X_clean = df[hour_cols].dropna()
outlier_flags = iso.fit_predict(X_clean)
n_outliers = (outlier_flags == -1).sum()
print(f"Isolation Forest flagged {n_outliers} outlier day-records ({n_outliers/len(X_clean):.2%})")

# Which users do these outliers cluster in?
outlier_df = df.loc[X_clean.index].copy()
outlier_df['is_outlier'] = outlier_flags == -1
outlier_df.groupby('user')['is_outlier'].mean().sort_values(ascending=False).head(10)
Out [15]:
Isolation Forest flagged 996 outlier day-records (2.00%)
user
42    0.088847
39    0.077572
34    0.070248
41    0.058824
48    0.050360
35    0.045455
37    0.045020
23    0.040493
15    0.038270
38    0.037796
Name: is_outlier, dtype: float64
In [16]:
# How does sequence length affect the number of users
# who have enough raw data?

sequence_lengths = [1, 3, 7, 14, 21, 30, 50]

results = []

for seq_len in sequence_lengths:
    usable_users = quality[quality['n_days'] >= seq_len]

    results.append({
        'sequence_length': seq_len,
        'usable_users': len(usable_users),
        'percentage_of_users': len(usable_users) / len(quality) * 100
    })

seq_df = pd.DataFrame(results)

print(seq_df)

plt.figure(figsize=(10, 5))
plt.plot(
    seq_df['sequence_length'],
    seq_df['percentage_of_users'],
    marker='o'
)

plt.xlabel('Sequence length (days)')
plt.ylabel('% of users with enough data')
plt.title('Effect of sequence length on usable users')
plt.ylim(0, 105)
plt.grid(True)
plt.show()
   sequence_length  usable_users  percentage_of_users
0                1            51           100.000000
1                3            51           100.000000
2                7            50            98.039216
3               14            50            98.039216
4               21            50            98.039216
5               30            50            98.039216
6               50            49            96.078431
In [24]:
import sys, types

# Stub out keras/tensorflow so importing main.py doesn't require
# installing the full TensorFlow library — we only need filter_and_preprocess_data,
# which has nothing to do with neural networks.
keras_mod = types.ModuleType('keras')
keras_mod.Input = object
sys.modules['keras'] = keras_mod
sys.modules['keras.src'] = types.ModuleType('keras.src')

losses_mod = types.ModuleType('keras.src.losses')
losses_mod.SparseCategoricalCrossentropy = object
sys.modules['keras.src.losses'] = losses_mod

metrics_mod = types.ModuleType('keras.src.metrics')
metrics_mod.SparseCategoricalAccuracy = object
sys.modules['keras.src.metrics'] = metrics_mod

sys.modules['tensorflow'] = types.ModuleType('tensorflow')
sys.modules['tensorflow.keras'] = types.ModuleType('tensorflow.keras')

tfkm = types.ModuleType('tensorflow.keras.models')
tfkm.Sequential = object
sys.modules['tensorflow.keras.models'] = tfkm

tfkl = types.ModuleType('tensorflow.keras.layers')
tfkl.LSTM = object
tfkl.Dense = object
tfkl.Bidirectional = object
tfkl.GRU = object
sys.modules['tensorflow.keras.layers'] = tfkl

tfko = types.ModuleType('tensorflow.keras.optimizers')
tfko.Adam = object
sys.modules['tensorflow.keras.optimizers'] = tfko

print("TensorFlow/Keras stubbed — main.py can now be imported without it")
TensorFlow/Keras stubbed — main.py can now be imported without it
In [26]:
%pip install "pandas==2.1.4"
Defaulting to user installation because normal site-packages is not writeable
Collecting pandas==2.1.4
  Downloading pandas-2.1.4.tar.gz (4.3 MB)
     ---------------------------------------- 0.0/4.3 MB ? eta -:--:--
     --------- ------------------------------ 1.0/4.3 MB 11.8 MB/s eta 0:00:01
     ------------------------------- -------- 3.4/4.3 MB 11.6 MB/s eta 0:00:01
     ---------------------------------------- 4.3/4.3 MB 10.9 MB/s  0:00:00
  Installing build dependencies: started
  Installing build dependencies: still running...
  Installing build dependencies: still running...
  Installing build dependencies: still running...
  Installing build dependencies: still running...
  Installing build dependencies: finished with status 'done'
  Getting requirements to build wheel: started
  Getting requirements to build wheel: finished with status 'done'
  Preparing metadata (pyproject.toml): started
  Preparing metadata (pyproject.toml): finished with status 'error'
Note: you may need to restart the kernel to use updated packages.
  error: subprocess-exited-with-error
  
  × Preparing metadata (pyproject.toml) did not run successfully.
  │ exit code: 1
  ╰─> [12 lines of output]
      + meson setup C:\Users\PMLS\AppData\Local\Temp\pip-install-27cw5jl9\pandas_7977c75aef884d0582454f16c9211d88 C:\Users\PMLS\AppData\Local\Temp\pip-install-27cw5jl9\pandas_7977c75aef884d0582454f16c9211d88\.mesonpy-zwd24j7z\build -Dbuildtype=release -Db_ndebug=if-release -Db_vscrt=md --vsenv --native-file=C:\Users\PMLS\AppData\Local\Temp\pip-install-27cw5jl9\pandas_7977c75aef884d0582454f16c9211d88\.mesonpy-zwd24j7z\build\meson-python-native-file.ini
      The Meson build system
      Version: 1.2.1
      Source dir: C:\Users\PMLS\AppData\Local\Temp\pip-install-27cw5jl9\pandas_7977c75aef884d0582454f16c9211d88
      Build dir: C:\Users\PMLS\AppData\Local\Temp\pip-install-27cw5jl9\pandas_7977c75aef884d0582454f16c9211d88\.mesonpy-zwd24j7z\build
      Build type: native build
      Project name: pandas
      Project version: 2.1.4
      
      ..\..\meson.build:2:0: ERROR: Could not find C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe
      
      A full log can be found at C:\Users\PMLS\AppData\Local\Temp\pip-install-27cw5jl9\pandas_7977c75aef884d0582454f16c9211d88\.mesonpy-zwd24j7z\build\meson-logs\meson-log.txt
      [end of output]
  
  note: This error originates from a subprocess, and is likely not a problem with pip.

[notice] A new release of pip is available: 26.1.2 -> 26.2.1
[notice] To update, run: python.exe -m pip install --upgrade pip
error: metadata-generation-failed

× Encountered error while generating package metadata.
╰─> pandas

note: This is an issue with the package mentioned above, not pip.
hint: See above for details.
In [28]:
# ============================================================
# Check how many users remain after the project's filtering
# ============================================================

# Start with the original EDA dataframe
project_df = df.copy()

print("Users in original dataset:", project_df['user'].nunique())

# ------------------------------------------------------------
# 1. Remove COVID-period data
# The project removes data from 2020 onwards.
# ------------------------------------------------------------

project_df = project_df[project_df['Year'] < 2020]

print("Users after removing COVID-period data:",
      project_df['user'].nunique())

# ------------------------------------------------------------
# 2. Apply the same hourly binning used by the project
# ------------------------------------------------------------

for hour in hour_cols:

    # Below 1000: round to nearest 10
    mask_low = project_df[hour] < 1000
    project_df.loc[mask_low, hour] = (
        (project_df.loc[mask_low, hour] / 10).round() * 10
    )

    # 1000–9999: round to nearest 100
    mask_mid = (
        (project_df[hour] >= 1000) &
        (project_df[hour] < 10000)
    )
    project_df.loc[mask_mid, hour] = (
        (project_df.loc[mask_mid, hour] / 100).round() * 100
    )

    # Above 10000: cap at 10000
    project_df.loc[project_df[hour] > 10000, hour] = 10000

# ------------------------------------------------------------
# 3. Remove duplicate rows
# The project uses the remaining unique rows to count
# usable datapoints for each user.
# ------------------------------------------------------------

cols_without_user = [
    col for col in project_df.columns
    if col != 'user'
]

reduced = project_df.drop_duplicates(
    subset=cols_without_user,
    keep=False
)

# ------------------------------------------------------------
# 4. Count usable datapoints for every user
# ------------------------------------------------------------

user_counts = reduced.groupby('user').size()

# Project threshold = 500 usable datapoints
users_kept = user_counts[user_counts >= 500]
users_removed = user_counts[user_counts < 500]

print("\nUsers kept by project:", len(users_kept))
print("Users removed by project:", len(users_removed))

print("\nUsers removed because they have fewer than 500 usable datapoints:")
print(users_removed.sort_index())
Users in original dataset: 51
Users after removing COVID-period data: 48

Users kept by project: 32
Users removed by project: 14

Users removed because they have fewer than 500 usable datapoints:
user
3     459
7     334
9       3
21    104
23    406
25    116
27    113
28    364
31    290
32    189
34     86
36    349
39    436
50    104
dtype: int64
In [29]:
# ============================================================
# User × Hour heatmap
# ============================================================

# Calculate average activity for each user at each hour
user_hour_mean = df.groupby('user')[hour_cols].mean()

plt.figure(figsize=(16, 12))

sns.heatmap(
    user_hour_mean,
    cmap='viridis',
    linewidths=0.2
)

plt.title('Average Hourly Activity Pattern per User')
plt.xlabel('Hour of Day')
plt.ylabel('User ID')

plt.show()
In [ ]: