├── .gitignore ├── Code ├── Extract Gravity Signal.ipynb ├── Phone Position Classification Exercise.ipynb ├── Process Smartphone Sensor Data.ipynb ├── Resample Sensor Data to 10 Hz Sampling Rate.ipynb ├── Rotate Sensor Data to Vehicle Reference Frame.ipynb ├── Vehicle Classification Exercise.ipynb ├── learnposition.py ├── learnvehicle.py ├── quatrotate.py └── sensordata.py ├── Data ├── nickandroid_exp2.csv ├── shanebus20150827.csv ├── shanebus20150827_processed.csv ├── shaneiphone_exp2.csv └── shaneiphone_exp2_processed.csv ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Byte-compiled / optimized / DLL files 3 | __pycache__/ 4 | *.py[cod] 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | 47 | # Translations 48 | *.mo 49 | *.pot 50 | 51 | # Django stuff: 52 | *.log 53 | 54 | # Sphinx documentation 55 | docs/_build/ 56 | 57 | # PyBuilder 58 | target/ 59 | 60 | # vim 61 | *.sw? 62 | 63 | # ipython notebook 64 | .ipynb* 65 | -------------------------------------------------------------------------------- /Code/Phone Position Classification Exercise.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Goal: Classify smartphone location as driver-side or passenger-side based on sensor data" 8 | ] 9 | }, 10 | { 11 | "cell_type": "code", 12 | "execution_count": 28, 13 | "metadata": { 14 | "collapsed": true 15 | }, 16 | "outputs": [], 17 | "source": [ 18 | "import pandas as pd\n", 19 | "from scipy.ndimage import gaussian_filter\n", 20 | "from sklearn.ensemble import RandomForestClassifier\n", 21 | "from sklearn.cross_validation import cross_val_score\n", 22 | "from sklearn.metrics import accuracy_score\n", 23 | "import numpy as np\n", 24 | "%matplotlib inline" 25 | ] 26 | }, 27 | { 28 | "cell_type": "markdown", 29 | "metadata": {}, 30 | "source": [ 31 | "#### Load the processed sensor data for car trip (see \"Process Smartphone Sensor Data\" jupyter notebook). On this trip, I drove my car from home to Censio and back and used SensorLog on my iPhone to track the trip. The total time for the trip was about 15 minutes." 32 | ] 33 | }, 34 | { 35 | "cell_type": "code", 36 | "execution_count": 29, 37 | "metadata": { 38 | "collapsed": false 39 | }, 40 | "outputs": [], 41 | "source": [ 42 | "df = pd.read_csv('../Data/shaneiphone_exp2_processed.csv', index_col='DateTime')" 43 | ] 44 | }, 45 | { 46 | "cell_type": "code", 47 | "execution_count": 30, 48 | "metadata": { 49 | "collapsed": true 50 | }, 51 | "outputs": [], 52 | "source": [ 53 | "# Use only userAcceleration and gyroscope data, since these features are expected to generalize well.\n", 54 | "xyz = ['X', 'Y', 'Z']\n", 55 | "measures = ['userAcceleration', 'gyroscope']\n", 56 | "basefeatures = [i + j for i in measures for j in xyz]\n", 57 | "features = [i + j for i in measures for j in xyz]" 58 | ] 59 | }, 60 | { 61 | "cell_type": "code", 62 | "execution_count": 31, 63 | "metadata": { 64 | "collapsed": true 65 | }, 66 | "outputs": [], 67 | "source": [ 68 | "# Add Gaussian smoothed features\n", 69 | "smoothfeatures = []\n", 70 | "for i in features:\n", 71 | " df[i + 'sm'] = gaussian_filter(df[i], 3)\n", 72 | " df[i + '2sm'] = gaussian_filter(df[i], 100)\n", 73 | " smoothfeatures.append(i + 'sm')\n", 74 | " smoothfeatures.append(i + '2sm')\n", 75 | "features.extend(smoothfeatures)" 76 | ] 77 | }, 78 | { 79 | "cell_type": "code", 80 | "execution_count": 32, 81 | "metadata": { 82 | "collapsed": true 83 | }, 84 | "outputs": [], 85 | "source": [ 86 | "# Generate Jerk signal\n", 87 | "jerkfeatures = []\n", 88 | "for i in features:\n", 89 | " diffsignal = np.diff(df[i])\n", 90 | " df[i + 'jerk'] = np.append(0, diffsignal)\n", 91 | " jerkfeatures.append(i + 'jerk')\n", 92 | "features.extend(jerkfeatures)" 93 | ] 94 | }, 95 | { 96 | "cell_type": "code", 97 | "execution_count": 33, 98 | "metadata": { 99 | "collapsed": true 100 | }, 101 | "outputs": [], 102 | "source": [ 103 | "# assign class labels\n", 104 | "class0 = (df.index > '2015-08-25 14:35:00') & \\\n", 105 | " (df.index < '2015-08-25 14:42:00')\n", 106 | "\n", 107 | "class1 = (df.index > '2015-08-25 14:43:00') & \\\n", 108 | " (df.index < '2015-08-25 14:48:00')\n", 109 | "\n", 110 | "df['class'] = -1\n", 111 | "df['class'][class0] = 0\n", 112 | "df['class'][class1] = 1" 113 | ] 114 | }, 115 | { 116 | "cell_type": "code", 117 | "execution_count": 37, 118 | "metadata": { 119 | "collapsed": true 120 | }, 121 | "outputs": [], 122 | "source": [ 123 | "# separate into quarters for train and validation\n", 124 | "q1 = df[(df.index <= '2015-08-25 14:38:30') & \n", 125 | " (df.index > '2015-08-25 14:33:00')]\n", 126 | "q2 = df[(df.index > '2015-08-25 14:38:30') & \n", 127 | " (df.index <= '2015-08-25 14:42:00')]\n", 128 | "q3 = df[(df.index > '2015-08-25 14:43:00') & \n", 129 | " (df.index <= '2015-08-25 14:45:30')]\n", 130 | "q4 = df[(df.index > '2015-08-25 14:45:30') & \n", 131 | " (df.index <= '2015-08-25 14:48:00')]\n", 132 | "traindf = pd.concat([q1, q3])\n", 133 | "validationdf = pd.concat([q2, q4])" 134 | ] 135 | }, 136 | { 137 | "cell_type": "code", 138 | "execution_count": 10, 139 | "metadata": { 140 | "collapsed": false 141 | }, 142 | "outputs": [ 143 | { 144 | "name": "stdout", 145 | "output_type": "stream", 146 | "text": [ 147 | "0\n", 148 | "0\n" 149 | ] 150 | } 151 | ], 152 | "source": [ 153 | "# check for NaNs in the dataframes\n", 154 | "print(traindf.isnull().sum().sum())\n", 155 | "print(validationdf.isnull().sum().sum())" 156 | ] 157 | }, 158 | { 159 | "cell_type": "code", 160 | "execution_count": 11, 161 | "metadata": { 162 | "collapsed": true 163 | }, 164 | "outputs": [], 165 | "source": [ 166 | "# drop NaNs\n", 167 | "traindf = traindf.dropna()\n", 168 | "validationdf = validationdf.dropna()" 169 | ] 170 | }, 171 | { 172 | "cell_type": "code", 173 | "execution_count": 12, 174 | "metadata": { 175 | "collapsed": true 176 | }, 177 | "outputs": [], 178 | "source": [ 179 | "# Make the training and validation sets\n", 180 | "X_train = traindf[features].values\n", 181 | "y_train = traindf['class'].values\n", 182 | "X_test = validationdf[features].values\n", 183 | "y_test = validationdf['class'].values" 184 | ] 185 | }, 186 | { 187 | "cell_type": "code", 188 | "execution_count": 13, 189 | "metadata": { 190 | "collapsed": false 191 | }, 192 | "outputs": [], 193 | "source": [ 194 | "# train a random forest\n", 195 | "clf = RandomForestClassifier(n_estimators=200)" 196 | ] 197 | }, 198 | { 199 | "cell_type": "code", 200 | "execution_count": 14, 201 | "metadata": { 202 | "collapsed": false 203 | }, 204 | "outputs": [ 205 | { 206 | "name": "stdout", 207 | "output_type": "stream", 208 | "text": [ 209 | "(array([ 0.76409945, 0.81746513, 0.97387606, 0.93256379, 0.92284326]), 0.88216953903356132, 0.078397989661162126)\n" 210 | ] 211 | } 212 | ], 213 | "source": [ 214 | "# get the 5-fold cross-validation score\n", 215 | "scores = cross_val_score(clf, X_train, y_train, cv=5)\n", 216 | "print(scores, scores.mean(), scores.std())" 217 | ] 218 | }, 219 | { 220 | "cell_type": "code", 221 | "execution_count": 15, 222 | "metadata": { 223 | "collapsed": true 224 | }, 225 | "outputs": [], 226 | "source": [ 227 | "# apply model to test set\n", 228 | "clf.fit(X_train, y_train)\n", 229 | "predict_y = clf.predict(X_test)" 230 | ] 231 | }, 232 | { 233 | "cell_type": "code", 234 | "execution_count": 16, 235 | "metadata": { 236 | "collapsed": false 237 | }, 238 | "outputs": [ 239 | { 240 | "name": "stdout", 241 | "output_type": "stream", 242 | "text": [ 243 | "Accuracy score on test set: 0.596\n" 244 | ] 245 | } 246 | ], 247 | "source": [ 248 | "# obtain accuracy score\n", 249 | "testscore = accuracy_score(y_test, predict_y)\n", 250 | "print(\"Accuracy score on test set: %6.3f\" % testscore)" 251 | ] 252 | }, 253 | { 254 | "cell_type": "markdown", 255 | "metadata": {}, 256 | "source": [ 257 | "#### 88% accuracy on the training set and 60% accuracy on the test set means we're overfitting the data. Accelerometer data should be the key here, since a phone on the driver side will experience a different centripetal acceleration than a phone on the passenger side during turns. But accelerometer data is super noisy!" 258 | ] 259 | }, 260 | { 261 | "cell_type": "code", 262 | "execution_count": 17, 263 | "metadata": { 264 | "collapsed": false 265 | }, 266 | "outputs": [ 267 | { 268 | "name": "stdout", 269 | "output_type": "stream", 270 | "text": [ 271 | "userAccelerationX: 0.0002\n", 272 | "userAccelerationY: 0.0003\n", 273 | "userAccelerationZ: 0.0006\n", 274 | "gyroscopeX: 0.0303\n", 275 | "gyroscopeY: 0.0023\n", 276 | "gyroscopeZ: 0.0113\n", 277 | "userAccelerationXsm: 0.0012\n", 278 | "userAccelerationX2sm: 0.0183\n", 279 | "userAccelerationYsm: 0.0013\n", 280 | "userAccelerationY2sm: 0.0155\n", 281 | "userAccelerationZsm: 0.0037\n", 282 | "userAccelerationZ2sm: 0.0536\n", 283 | "gyroscopeXsm: 0.1349\n", 284 | "gyroscopeX2sm: 0.2690\n", 285 | "gyroscopeYsm: 0.0080\n", 286 | "gyroscopeY2sm: 0.0775\n", 287 | "gyroscopeZsm: 0.0392\n", 288 | "gyroscopeZ2sm: 0.1916\n", 289 | "userAccelerationXjerk: 0.0002\n", 290 | "userAccelerationYjerk: 0.0004\n", 291 | "userAccelerationZjerk: 0.0003\n", 292 | "gyroscopeXjerk: 0.0010\n", 293 | "gyroscopeYjerk: 0.0009\n", 294 | "gyroscopeZjerk: 0.0004\n", 295 | "userAccelerationXsmjerk: 0.0004\n", 296 | "userAccelerationX2smjerk: 0.0119\n", 297 | "userAccelerationYsmjerk: 0.0004\n", 298 | "userAccelerationY2smjerk: 0.0203\n", 299 | "userAccelerationZsmjerk: 0.0007\n", 300 | "userAccelerationZ2smjerk: 0.0103\n", 301 | "gyroscopeXsmjerk: 0.0003\n", 302 | "gyroscopeX2smjerk: 0.0172\n", 303 | "gyroscopeYsmjerk: 0.0007\n", 304 | "gyroscopeY2smjerk: 0.0464\n", 305 | "gyroscopeZsmjerk: 0.0010\n", 306 | "gyroscopeZ2smjerk: 0.0282\n" 307 | ] 308 | } 309 | ], 310 | "source": [ 311 | "# Inspect feature importances\n", 312 | "for i, ifeature in enumerate(features):\n", 313 | " print(ifeature + ': %6.4f' % clf.feature_importances_[i])" 314 | ] 315 | }, 316 | { 317 | "cell_type": "markdown", 318 | "metadata": {}, 319 | "source": [ 320 | "#### The smoothed gyroscopeX data is the most useful feature. This is further confirmation of over-fitting, since this feature corresponds to pitch angle rotation which should be negligible in this dataset (the pitch of the car never changed significantly during the drive)." 321 | ] 322 | }, 323 | { 324 | "cell_type": "code", 325 | "execution_count": 19, 326 | "metadata": { 327 | "collapsed": false 328 | }, 329 | "outputs": [ 330 | { 331 | "data": { 332 | "text/plain": [ 333 | "" 334 | ] 335 | }, 336 | "execution_count": 19, 337 | "metadata": {}, 338 | "output_type": "execute_result" 339 | }, 340 | { 341 | "data": { 342 | "image/png": "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\nb+QKkSXWzdJklbJN7KJYLw7r9f/1r/pusY9tDkCWz3pLi1YAnZ16782CkWT79rhYnz9fl2W//HK1\nHL49Cr9t21i+pYkr53TSTNJSkcamTVpx3nef/62quxHVwxahyekE0/Z2rcjWr/Viva0NmPTrt1D7\n5lE70mwIJLKsV2VGEhBXi7o64BvfULERVq577x23yG3dCmzfWoNqqdlRuba1xQW9RRypq9PnctEi\n708YdkRsaWlrOACt6EeMyJ2Y95e/qKV+1CjvW3zrrSrskr64PWXyiMl9uippGrNne6u6+Rdb5Ihk\neXJOy+uQIdmW9ZEj9b5afThjhr6snC9erAKztVUb9LY23fett+KWdUD/GzFCtxs2TNP3u99px3jT\nJj3HggW63Xe/qxGsRozQPLzhBn1Oli/X75Mnaxkxq3HYYJoIXbNGO3dmGeur+OChZf3IIzX6lhGK\nklGj4iM+K1fGwwKbm1FoWf/+9/W35IJ3TzzhtzeXgSVL4mL9pZeAM87wbYDNXTA3mLTRpzBIwdvf\nDlx2mf++fn1crNtIi3M+UsfWrXEBVUxkpr5i82a17mdh9Yq1n6tWqevjCy+owEyK8IMP1npk3Tpd\nsXf5chWktkjUm2/6SbcHH6zGuE9/OtcVJNlGJF0wwhHsNDcYG2kx6uv9s57ms25uMDZvZc2a3Ggw\nScu6+a6br3hahwTwE0ytzg0t65b3FgEptFCH1zh2rKbbOXW5sihRNnF15Up/vbYQpJWxcHRpt918\nh8VGEpJi3ep6a1+2b08X6y0t6l3R0ACcf76fw/PNb+oI3d//rotRffjDwJlnxvcNV483bEGxtWs1\n7ffco3MiAd02n/GNYr0AhxyigiqctFkKF1ygBf6Gd9yAe//13sztbHJIllgPH/QssW6YWG9s1MLa\n3U2xXgw/+IGKZhNp5vpkq51m+aybWDef9ZqabDeYtHBd3/qWNiYmJK2iNLFuK9gaU6Z4cZpvSXFA\nI2asXKnx8pctC+JhdzUCrYuwedVI/OY3eg1DhwJr13ix3t4ObNvQjq1btYGw0YClS9Wy3lXADUa6\n61FbqxXcf/yHX5AHUMt1OPHOFmtpb0dMrIfW19de88/h2LEqSkysh5MHLQSqNRxz5qi1EtBnOWTZ\nMp3sOGmSb9B/+1utuLMsYgPBs8+WZhF+3/t89KawAW1s1EY7zLOtW/W3rMlX06Zp/q9f7+upGTNU\n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\n", 343 | "text/plain": [ 344 | "" 345 | ] 346 | }, 347 | "metadata": {}, 348 | "output_type": "display_data" 349 | } 350 | ], 351 | "source": [ 352 | "# compare bus gyroscopeZ2sm and car gyroscopeZ2sm\n", 353 | "#q1['gyroscopeXsm'].plot(color='blue', figsize=(12,6), kind='hist', bins=40, alpha=0.4) # car\n", 354 | "#q3['gyroscopeXsm'].plot(color='green', kind='hist', bins=40, alpha=0.4) # bus\n", 355 | "q1['gyroscopeXsm'].plot(color='blue', figsize=(12,6)) # car\n", 356 | "q3['gyroscopeXsm'].plot(color='green') # bus" 357 | ] 358 | }, 359 | { 360 | "cell_type": "markdown", 361 | "metadata": {}, 362 | "source": [ 363 | "#### This seems like an enormous difference in pitch angle and is really hard to understand. My guess is that it is somehow an artifact of the experimental setup. Perhaps the quaternion rotation of gyroscopeXYZ is not quite perfect." 364 | ] 365 | }, 366 | { 367 | "cell_type": "markdown", 368 | "metadata": {}, 369 | "source": [ 370 | "#### Another interesting avenue to pursue is features in Fourier space" 371 | ] 372 | }, 373 | { 374 | "cell_type": "code", 375 | "execution_count": 35, 376 | "metadata": { 377 | "collapsed": false 378 | }, 379 | "outputs": [], 380 | "source": [ 381 | "# Generate Fourier Transform of features\n", 382 | "fftfeatures = []\n", 383 | "for i in features:\n", 384 | " reals = np.real(np.fft.rfft(df[i]))\n", 385 | " imags = np.imag(np.fft.rfft(df[i]))\n", 386 | " complexs = [reals[0]]\n", 387 | " n = len(reals)\n", 388 | " if n % 2 == 0:\n", 389 | " complexs.append(imags[0])\n", 390 | " for j in range(1, n - 1):\n", 391 | " complexs.append(reals[j])\n", 392 | " complexs.append(imags[j])\n", 393 | " complexs.append(reals[j])\n", 394 | " if len(df) > len(complexs):\n", 395 | " complexs.append(imags[j])\n", 396 | " df['f' + i] = complexs\n", 397 | " fftfeatures.append('f' + i)\n", 398 | "features.extend(fftfeatures)" 399 | ] 400 | }, 401 | { 402 | "cell_type": "code", 403 | "execution_count": 38, 404 | "metadata": { 405 | "collapsed": true 406 | }, 407 | "outputs": [], 408 | "source": [ 409 | "# separate into quarters for train and validation\n", 410 | "q1 = df[(df.index <= '2015-08-25 14:38:30') & \n", 411 | " (df.index > '2015-08-25 14:33:00')]\n", 412 | "q2 = df[(df.index > '2015-08-25 14:38:30') & \n", 413 | " (df.index <= '2015-08-25 14:42:00')]\n", 414 | "q3 = df[(df.index > '2015-08-25 14:43:00') & \n", 415 | " (df.index <= '2015-08-25 14:45:30')]\n", 416 | "q4 = df[(df.index > '2015-08-25 14:45:30') & \n", 417 | " (df.index <= '2015-08-25 14:48:00')]\n", 418 | "traindf = pd.concat([q1, q3])\n", 419 | "validationdf = pd.concat([q2, q4])" 420 | ] 421 | }, 422 | { 423 | "cell_type": "code", 424 | "execution_count": 39, 425 | "metadata": { 426 | "collapsed": false 427 | }, 428 | "outputs": [], 429 | "source": [ 430 | "# Make the training and validation sets\n", 431 | "X_train = traindf[fftfeatures].values\n", 432 | "y_train = traindf['class'].values\n", 433 | "X_test = validationdf[fftfeatures].values\n", 434 | "y_test = validationdf['class'].values" 435 | ] 436 | }, 437 | { 438 | "cell_type": "code", 439 | "execution_count": 40, 440 | "metadata": { 441 | "collapsed": false 442 | }, 443 | "outputs": [], 444 | "source": [ 445 | "# train a random forest\n", 446 | "clf = RandomForestClassifier(n_estimators=200)" 447 | ] 448 | }, 449 | { 450 | "cell_type": "code", 451 | "execution_count": 41, 452 | "metadata": { 453 | "collapsed": false 454 | }, 455 | "outputs": [ 456 | { 457 | "name": "stdout", 458 | "output_type": "stream", 459 | "text": [ 460 | "(array([ 0.76773802, 1. , 1. , 0.97934386, 0.87302552]), 0.92402148067203727, 0.091248171911248468)\n" 461 | ] 462 | } 463 | ], 464 | "source": [ 465 | "# get the 5-fold cross-validation score\n", 466 | "scores = cross_val_score(clf, X_train, y_train, cv=5)\n", 467 | "print(scores, scores.mean(), scores.std())" 468 | ] 469 | }, 470 | { 471 | "cell_type": "code", 472 | "execution_count": 42, 473 | "metadata": { 474 | "collapsed": true 475 | }, 476 | "outputs": [], 477 | "source": [ 478 | "# apply model to test set\n", 479 | "clf.fit(X_train, y_train)\n", 480 | "predict_y = clf.predict(X_test)" 481 | ] 482 | }, 483 | { 484 | "cell_type": "code", 485 | "execution_count": 43, 486 | "metadata": { 487 | "collapsed": false 488 | }, 489 | "outputs": [ 490 | { 491 | "name": "stdout", 492 | "output_type": "stream", 493 | "text": [ 494 | "Accuracy score on test set: 0.731\n" 495 | ] 496 | } 497 | ], 498 | "source": [ 499 | "# obtain accuracy score\n", 500 | "testscore = accuracy_score(y_test, predict_y)\n", 501 | "print(\"Accuracy score on test set: %6.3f\" % testscore)" 502 | ] 503 | }, 504 | { 505 | "cell_type": "markdown", 506 | "metadata": {}, 507 | "source": [ 508 | "#### Better accuracy on the test set: 73%. We are still overfitting here, since we got 92% accuracy on the training set. Using the Fourier domain seems to be helpful." 509 | ] 510 | }, 511 | { 512 | "cell_type": "code", 513 | "execution_count": 44, 514 | "metadata": { 515 | "collapsed": false 516 | }, 517 | "outputs": [ 518 | { 519 | "name": "stdout", 520 | "output_type": "stream", 521 | "text": [ 522 | "fuserAccelerationX: 0.0009\n", 523 | "fuserAccelerationY: 0.0010\n", 524 | "fuserAccelerationZ: 0.0010\n", 525 | "fgyroscopeX: 0.0011\n", 526 | "fgyroscopeY: 0.0013\n", 527 | "fgyroscopeZ: 0.0031\n", 528 | "fuserAccelerationXsm: 0.0387\n", 529 | "fuserAccelerationX2sm: 0.0641\n", 530 | "fuserAccelerationYsm: 0.0299\n", 531 | "fuserAccelerationY2sm: 0.0614\n", 532 | "fuserAccelerationZsm: 0.0245\n", 533 | "fuserAccelerationZ2sm: 0.0598\n", 534 | "fgyroscopeXsm: 0.0374\n", 535 | "fgyroscopeX2sm: 0.0661\n", 536 | "fgyroscopeYsm: 0.0436\n", 537 | "fgyroscopeY2sm: 0.0424\n", 538 | "fgyroscopeZsm: 0.0531\n", 539 | "fgyroscopeZ2sm: 0.0595\n", 540 | "fuserAccelerationXjerk: 0.0041\n", 541 | "fuserAccelerationYjerk: 0.0022\n", 542 | "fuserAccelerationZjerk: 0.0027\n", 543 | "fgyroscopeXjerk: 0.0032\n", 544 | "fgyroscopeYjerk: 0.0043\n", 545 | "fgyroscopeZjerk: 0.0022\n", 546 | "fuserAccelerationXsmjerk: 0.0577\n", 547 | "fuserAccelerationX2smjerk: 0.0279\n", 548 | "fuserAccelerationYsmjerk: 0.0371\n", 549 | "fuserAccelerationY2smjerk: 0.0020\n", 550 | "fuserAccelerationZsmjerk: 0.0256\n", 551 | "fuserAccelerationZ2smjerk: 0.0012\n", 552 | "fgyroscopeXsmjerk: 0.0422\n", 553 | "fgyroscopeX2smjerk: 0.0177\n", 554 | "fgyroscopeYsmjerk: 0.0583\n", 555 | "fgyroscopeY2smjerk: 0.0104\n", 556 | "fgyroscopeZsmjerk: 0.0391\n", 557 | "fgyroscopeZ2smjerk: 0.0731\n" 558 | ] 559 | } 560 | ], 561 | "source": [ 562 | "# Inspect feature importances\n", 563 | "for i, ifeature in enumerate(fftfeatures):\n", 564 | " print(ifeature + ': %6.4f' % clf.feature_importances_[i])" 565 | ] 566 | }, 567 | { 568 | "cell_type": "markdown", 569 | "metadata": {}, 570 | "source": [ 571 | "#### There is no single feature that is particularly important. This helps me feel more confident that the results will generalize well to new samples. 74% accuracy in 5 minute ride samples isn't too bad!" 572 | ] 573 | }, 574 | { 575 | "cell_type": "code", 576 | "execution_count": null, 577 | "metadata": { 578 | "collapsed": true 579 | }, 580 | "outputs": [], 581 | "source": [] 582 | } 583 | ], 584 | "metadata": { 585 | "kernelspec": { 586 | "display_name": "Python 2", 587 | "language": "python", 588 | "name": "python2" 589 | }, 590 | "language_info": { 591 | "codemirror_mode": { 592 | "name": "ipython", 593 | "version": 2 594 | }, 595 | "file_extension": ".py", 596 | "mimetype": "text/x-python", 597 | "name": "python", 598 | "nbconvert_exporter": "python", 599 | "pygments_lexer": "ipython2", 600 | "version": "2.7.10" 601 | } 602 | }, 603 | "nbformat": 4, 604 | "nbformat_minor": 0 605 | } 606 | -------------------------------------------------------------------------------- /Code/Process Smartphone Sensor Data.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Standardize sensor data from smartphone apps" 8 | ] 9 | }, 10 | { 11 | "cell_type": "code", 12 | "execution_count": 1, 13 | "metadata": { 14 | "collapsed": true 15 | }, 16 | "outputs": [], 17 | "source": [ 18 | "from sensordata import Load" 19 | ] 20 | }, 21 | { 22 | "cell_type": "code", 23 | "execution_count": 2, 24 | "metadata": { 25 | "collapsed": false 26 | }, 27 | "outputs": [ 28 | { 29 | "name": "stdout", 30 | "output_type": "stream", 31 | "text": [ 32 | "Reading csv file into Pandas dataframe\n", 33 | "Rotating to common reference frame...\n", 34 | "...accelerometer\n", 35 | "...orientation\n", 36 | "...userAcceleration\n", 37 | "...gravity\n", 38 | "...magneticField\n", 39 | "...gyroscope\n", 40 | "Resampling to 10 Hz\n", 41 | "Storing dataframe with modified date column to ../Data/shaneiphone_exp2_processed.csv\n" 42 | ] 43 | } 44 | ], 45 | "source": [ 46 | "# process Shane's iPhone data from car trip: Cambridgeport to Censio and back.\n", 47 | "dfloc = '../Data/shaneiphone_exp2.csv'\n", 48 | "df_processed = Load(dfloc)" 49 | ] 50 | }, 51 | { 52 | "cell_type": "code", 53 | "execution_count": 3, 54 | "metadata": { 55 | "collapsed": false 56 | }, 57 | "outputs": [ 58 | { 59 | "name": "stdout", 60 | "output_type": "stream", 61 | "text": [ 62 | "Reading csv file into Pandas dataframe\n", 63 | "Rotating to common reference frame...\n", 64 | "...accelerometer\n", 65 | "...orientation\n", 66 | "...userAcceleration\n", 67 | "...gravity\n", 68 | "...magneticField\n", 69 | "...gyroscope\n", 70 | "Resampling to 10 Hz\n", 71 | "Storing dataframe with modified date column to ../Data/shanebus20150827_processed.csv\n" 72 | ] 73 | } 74 | ], 75 | "source": [ 76 | "# process Shane's iPhone data from bus trip: Route 47.\n", 77 | "dfloc = '../Data/shanebus20150827.csv'\n", 78 | "df_processed = Load(dfloc)" 79 | ] 80 | }, 81 | { 82 | "cell_type": "code", 83 | "execution_count": null, 84 | "metadata": { 85 | "collapsed": true 86 | }, 87 | "outputs": [], 88 | "source": [] 89 | } 90 | ], 91 | "metadata": { 92 | "kernelspec": { 93 | "display_name": "Python 2", 94 | "language": "python", 95 | "name": "python2" 96 | }, 97 | "language_info": { 98 | "codemirror_mode": { 99 | "name": "ipython", 100 | "version": 2 101 | }, 102 | "file_extension": ".py", 103 | "mimetype": "text/x-python", 104 | "name": "python", 105 | "nbconvert_exporter": "python", 106 | "pygments_lexer": "ipython2", 107 | "version": "2.7.10" 108 | } 109 | }, 110 | "nbformat": 4, 111 | "nbformat_minor": 0 112 | } 113 | -------------------------------------------------------------------------------- /Code/Resample Sensor Data to 10 Hz Sampling Rate.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Goal: resample XYZ signals to 10 Hz sampling rate" 8 | ] 9 | }, 10 | { 11 | "cell_type": "markdown", 12 | "metadata": {}, 13 | "source": [ 14 | "#### Experiment: I drove my car from home to Censio and back. My phone rested on my seat facing forwards for the trip to Censio. Nick was in the passenger seat with his phone in his pocket. For the return trip, we swapped phones. The total time for the trip was about 15 minutes." 15 | ] 16 | }, 17 | { 18 | "cell_type": "code", 19 | "execution_count": 33, 20 | "metadata": { 21 | "collapsed": true 22 | }, 23 | "outputs": [], 24 | "source": [ 25 | "import pandas as pd\n", 26 | "import numpy as np\n", 27 | "%matplotlib inline\n", 28 | "import matplotlib.pyplot as plt" 29 | ] 30 | }, 31 | { 32 | "cell_type": "code", 33 | "execution_count": 34, 34 | "metadata": { 35 | "collapsed": true 36 | }, 37 | "outputs": [], 38 | "source": [ 39 | "# load the raw data\n", 40 | "df = pd.read_csv('../Data/shaneiphone_exp2.csv')" 41 | ] 42 | }, 43 | { 44 | "cell_type": "markdown", 45 | "metadata": {}, 46 | "source": [ 47 | "#### For SensorLog data, sampling rate can be obtained directly from 'motionTimestamp_sinceReboot'" 48 | ] 49 | }, 50 | { 51 | "cell_type": "code", 52 | "execution_count": 35, 53 | "metadata": { 54 | "collapsed": false 55 | }, 56 | "outputs": [], 57 | "source": [ 58 | "sampling_rate = 1 / np.diff(df['motionTimestamp_sinceReboot'])" 59 | ] 60 | }, 61 | { 62 | "cell_type": "code", 63 | "execution_count": 36, 64 | "metadata": { 65 | "collapsed": false 66 | }, 67 | "outputs": [ 68 | { 69 | "data": { 70 | "text/plain": [ 71 | "(array([ 1.40000000e+02, 2.00000000e+00, 1.45680000e+04,\n", 72 | " 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,\n", 73 | " 0.00000000e+00, 4.00000000e+00, 0.00000000e+00,\n", 74 | " 3.19800000e+03]),\n", 75 | " array([ 9.92073334, 12.08201557, 14.2432978 , 16.40458003,\n", 76 | " 18.56586226, 20.72714449, 22.88842673, 25.04970896,\n", 77 | " 27.21099119, 29.37227342, 31.53355565]),\n", 78 | " )" 79 | ] 80 | }, 81 | "execution_count": 36, 82 | "metadata": {}, 83 | "output_type": "execute_result" 84 | }, 85 | { 86 | "data": { 87 | "image/png": "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\n", 88 | "text/plain": [ 89 | "" 90 | ] 91 | }, 92 | "metadata": {}, 93 | "output_type": "display_data" 94 | } 95 | ], 96 | "source": [ 97 | "plt.hist(sampling_rate)" 98 | ] 99 | }, 100 | { 101 | "cell_type": "markdown", 102 | "metadata": {}, 103 | "source": [ 104 | "#### The sampling rate was 15 Hz most of the time. Sometimes it was 30 Hz. Very occasionally it was close to 10 Hz. This is somewhat annoying, so let's resample to 10 Hz." 105 | ] 106 | }, 107 | { 108 | "cell_type": "markdown", 109 | "metadata": {}, 110 | "source": [ 111 | "#### First, we have to make a DateTime column from the loggingTime columns and set it as the index" 112 | ] 113 | }, 114 | { 115 | "cell_type": "code", 116 | "execution_count": 37, 117 | "metadata": { 118 | "collapsed": true 119 | }, 120 | "outputs": [], 121 | "source": [ 122 | "df['DateTime'] = pd.DatetimeIndex(df['loggingTime'])\n", 123 | "df = df.set_index('DateTime')" 124 | ] 125 | }, 126 | { 127 | "cell_type": "markdown", 128 | "metadata": { 129 | "collapsed": true 130 | }, 131 | "source": [ 132 | "#### Then we just use pandas built-in resampling function" 133 | ] 134 | }, 135 | { 136 | "cell_type": "code", 137 | "execution_count": 38, 138 | "metadata": { 139 | "collapsed": false 140 | }, 141 | "outputs": [], 142 | "source": [ 143 | "# 100L corresponds to 100 milliseconds\n", 144 | "rdf = df.resample('100L')" 145 | ] 146 | }, 147 | { 148 | "cell_type": "code", 149 | "execution_count": 45, 150 | "metadata": { 151 | "collapsed": false 152 | }, 153 | "outputs": [ 154 | { 155 | "data": { 156 | "text/plain": [ 157 | "loggingSample 3\n", 158 | "locationTimestamp_since1970 3\n", 159 | "locationLatitude 3\n", 160 | "locationLongitude 3\n", 161 | "locationAltitude 3\n", 162 | "locationSpeed 3\n", 163 | "locationCourse 3\n", 164 | "locationVerticalAccuracy 3\n", 165 | "locationHorizontalAccuracy 3\n", 166 | "locationFloor 3\n", 167 | "locationHeadingTimestamp_since1970 3\n", 168 | "locationHeadingX 3\n", 169 | "locationHeadingY 3\n", 170 | "locationHeadingZ 3\n", 171 | "locationTrueHeading 3\n", 172 | "locationMagneticHeading 3\n", 173 | "locationHeadingAccuracy 3\n", 174 | "accelerometerTimestamp_sinceReboot 3\n", 175 | "accelerometerAccelerationX 3\n", 176 | "accelerometerAccelerationY 3\n", 177 | "accelerometerAccelerationZ 3\n", 178 | "gyroTimestamp_sinceReboot 3\n", 179 | "gyroRotationX 3\n", 180 | "gyroRotationY 3\n", 181 | "gyroRotationZ 3\n", 182 | "motionTimestamp_sinceReboot 3\n", 183 | "motionYaw 3\n", 184 | "motionRoll 3\n", 185 | "motionPitch 3\n", 186 | "motionRotationRateX 3\n", 187 | "motionRotationRateY 3\n", 188 | "motionRotationRateZ 3\n", 189 | "motionUserAccelerationX 3\n", 190 | "motionUserAccelerationY 3\n", 191 | "motionUserAccelerationZ 3\n", 192 | "motionQuaternionX 3\n", 193 | "motionQuaternionY 3\n", 194 | "motionQuaternionZ 3\n", 195 | "motionQuaternionW 3\n", 196 | "motionGravityX 3\n", 197 | "motionGravityY 3\n", 198 | "motionGravityZ 3\n", 199 | "motionMagneticFieldX 3\n", 200 | "motionMagneticFieldY 3\n", 201 | "motionMagneticFieldZ 3\n", 202 | "motionMagneticFieldCalibrationAccuracy 3\n", 203 | "activityTimestamp_sinceReboot 3\n", 204 | "activityActivityConfidence 3\n", 205 | "pedometerNumberofSteps 3\n", 206 | "pedometerDistance 3\n", 207 | "pedometerFloorAscended 3\n", 208 | "pedometerFloorDescended 3\n", 209 | "deviceOrientation 3\n", 210 | "batteryState 3\n", 211 | "batteryLevel 3\n", 212 | "state 3\n", 213 | "dtype: int64" 214 | ] 215 | }, 216 | "execution_count": 45, 217 | "metadata": {}, 218 | "output_type": "execute_result" 219 | } 220 | ], 221 | "source": [ 222 | "# check for nans\n", 223 | "rdf.isnull().sum()" 224 | ] 225 | }, 226 | { 227 | "cell_type": "markdown", 228 | "metadata": {}, 229 | "source": [ 230 | "#### 3 rows in the resampled database are nan's. Just drop them." 231 | ] 232 | }, 233 | { 234 | "cell_type": "code", 235 | "execution_count": 47, 236 | "metadata": { 237 | "collapsed": false 238 | }, 239 | "outputs": [], 240 | "source": [ 241 | "rdf = rdf.dropna()" 242 | ] 243 | }, 244 | { 245 | "cell_type": "code", 246 | "execution_count": 48, 247 | "metadata": { 248 | "collapsed": true 249 | }, 250 | "outputs": [], 251 | "source": [ 252 | "resampled_rate = 1 / np.diff(rdf['motionTimestamp_sinceReboot'])" 253 | ] 254 | }, 255 | { 256 | "cell_type": "code", 257 | "execution_count": 51, 258 | "metadata": { 259 | "collapsed": false 260 | }, 261 | "outputs": [ 262 | { 263 | "data": { 264 | "text/plain": [ 265 | "(array([ 2.40000000e+01, 2.15500000e+03, 1.64000000e+03,\n", 266 | " 4.53600000e+03, 1.00000000e+00, 1.00200000e+03,\n", 267 | " 1.00000000e+00, 1.04100000e+03, 0.00000000e+00,\n", 268 | " 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,\n", 269 | " 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,\n", 270 | " 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,\n", 271 | " 0.00000000e+00, 2.80000000e+01]),\n", 272 | " array([ 5.46289872, 6.76143832, 8.05997791, 9.35851751,\n", 273 | " 10.6570571 , 11.9555967 , 13.25413629, 14.55267589,\n", 274 | " 15.85121548, 17.14975508, 18.44829467, 19.74683427,\n", 275 | " 21.04537386, 22.34391346, 23.64245305, 24.94099265,\n", 276 | " 26.23953224, 27.53807184, 28.83661143, 30.13515103, 31.43369062]),\n", 277 | " )" 278 | ] 279 | }, 280 | "execution_count": 51, 281 | "metadata": {}, 282 | "output_type": "execute_result" 283 | }, 284 | { 285 | "data": { 286 | "image/png": "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\n", 287 | "text/plain": [ 288 | "" 289 | ] 290 | }, 291 | "metadata": {}, 292 | "output_type": "display_data" 293 | } 294 | ], 295 | "source": [ 296 | "plt.hist(resampled_rate, bins=20)" 297 | ] 298 | }, 299 | { 300 | "cell_type": "markdown", 301 | "metadata": {}, 302 | "source": [ 303 | "#### This seems like pretty lousy performance, but the DateTime indices themselves increment by 0.1 second with each row. We'll proceed for now, but keep this in mind later if there are problems further down the road." 304 | ] 305 | }, 306 | { 307 | "cell_type": "code", 308 | "execution_count": null, 309 | "metadata": { 310 | "collapsed": true 311 | }, 312 | "outputs": [], 313 | "source": [] 314 | } 315 | ], 316 | "metadata": { 317 | "kernelspec": { 318 | "display_name": "Python 2", 319 | "language": "python", 320 | "name": "python2" 321 | }, 322 | "language_info": { 323 | "codemirror_mode": { 324 | "name": "ipython", 325 | "version": 2 326 | }, 327 | "file_extension": ".py", 328 | "mimetype": "text/x-python", 329 | "name": "python", 330 | "nbconvert_exporter": "python", 331 | "pygments_lexer": "ipython2", 332 | "version": "2.7.10" 333 | } 334 | }, 335 | "nbformat": 4, 336 | "nbformat_minor": 0 337 | } 338 | -------------------------------------------------------------------------------- /Code/Vehicle Classification Exercise.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Goal: Classify vehicle as bus or car based on smartphone sensor data" 8 | ] 9 | }, 10 | { 11 | "cell_type": "code", 12 | "execution_count": 52, 13 | "metadata": { 14 | "collapsed": true 15 | }, 16 | "outputs": [], 17 | "source": [ 18 | "import pandas as pd\n", 19 | "from scipy.ndimage import gaussian_filter\n", 20 | "from sklearn.ensemble import RandomForestClassifier\n", 21 | "from sklearn.cross_validation import cross_val_score\n", 22 | "from sklearn.metrics import accuracy_score\n", 23 | "import numpy as np\n", 24 | "%matplotlib inline" 25 | ] 26 | }, 27 | { 28 | "cell_type": "markdown", 29 | "metadata": {}, 30 | "source": [ 31 | "#### Load the processed sensor data for car trip (see \"Process Smartphone Sensor Data\" jupyter notebook). On this trip, I drove my car from home to Censio and back and used SensorLog on my iPhone to track the trip. The total time for the trip was about 15 minutes." 32 | ] 33 | }, 34 | { 35 | "cell_type": "code", 36 | "execution_count": 20, 37 | "metadata": { 38 | "collapsed": false 39 | }, 40 | "outputs": [], 41 | "source": [ 42 | "dfcar = pd.read_csv('../Data/shaneiphone_exp2_processed.csv', index_col='DateTime')" 43 | ] 44 | }, 45 | { 46 | "cell_type": "markdown", 47 | "metadata": {}, 48 | "source": [ 49 | "#### Load the processed sensor data for bus trip (see \"Process Smartphone Sensor Data\" jupyter notebook). On this trip, I took the 47 bus for about 10 minutes." 50 | ] 51 | }, 52 | { 53 | "cell_type": "code", 54 | "execution_count": 21, 55 | "metadata": { 56 | "collapsed": false 57 | }, 58 | "outputs": [], 59 | "source": [ 60 | "dfbus = pd.read_csv('../Data/shanebus20150827_processed.csv', index_col='DateTime')" 61 | ] 62 | }, 63 | { 64 | "cell_type": "code", 65 | "execution_count": 22, 66 | "metadata": { 67 | "collapsed": true 68 | }, 69 | "outputs": [], 70 | "source": [ 71 | "# combine into a single dataframe\n", 72 | "df = pd.concat([dfcar, dfbus])" 73 | ] 74 | }, 75 | { 76 | "cell_type": "code", 77 | "execution_count": 38, 78 | "metadata": { 79 | "collapsed": true 80 | }, 81 | "outputs": [], 82 | "source": [ 83 | "# Use only userAcceleration and gyroscope data, since these features are expected to generalize well.\n", 84 | "xyz = ['X', 'Y', 'Z']\n", 85 | "measures = ['userAcceleration', 'gyroscope']\n", 86 | "basefeatures = [i + j for i in measures for j in xyz]\n", 87 | "features = [i + j for i in measures for j in xyz]" 88 | ] 89 | }, 90 | { 91 | "cell_type": "code", 92 | "execution_count": 39, 93 | "metadata": { 94 | "collapsed": true 95 | }, 96 | "outputs": [], 97 | "source": [ 98 | "# Add Gaussian smoothed features\n", 99 | "smoothfeatures = []\n", 100 | "for i in features:\n", 101 | " df[i + 'sm'] = gaussian_filter(df[i], 3)\n", 102 | " df[i + '2sm'] = gaussian_filter(df[i], 100)\n", 103 | " smoothfeatures.append(i + 'sm')\n", 104 | " smoothfeatures.append(i + '2sm')\n", 105 | "features.extend(smoothfeatures)" 106 | ] 107 | }, 108 | { 109 | "cell_type": "code", 110 | "execution_count": 40, 111 | "metadata": { 112 | "collapsed": true 113 | }, 114 | "outputs": [], 115 | "source": [ 116 | "# Generate Jerk signal\n", 117 | "jerkfeatures = []\n", 118 | "for i in features:\n", 119 | " diffsignal = np.diff(df[i])\n", 120 | " df[i + 'jerk'] = np.append(0, diffsignal)\n", 121 | " jerkfeatures.append(i + 'jerk')\n", 122 | "features.extend(jerkfeatures)" 123 | ] 124 | }, 125 | { 126 | "cell_type": "code", 127 | "execution_count": 41, 128 | "metadata": { 129 | "collapsed": true 130 | }, 131 | "outputs": [], 132 | "source": [ 133 | "# assign class labels\n", 134 | "car0 = (df.index > '2015-08-25 14:35:00') & \\\n", 135 | " (df.index <= '2015-08-25 14:42:00')\n", 136 | "\n", 137 | "car1 = (df.index > '2015-08-25 14:43:00') & \\\n", 138 | " (df.index <= '2015-08-25 14:48:00')\n", 139 | "\n", 140 | "bus0 = (df.index > '2015-08-27 10:10:00') & \\\n", 141 | " (df.index <= '2015-08-27 10:15:00')\n", 142 | "bus1 = (df.index > '2015-08-27 10:15:00') & \\\n", 143 | " (df.index <= '2015-08-27 10:20:00')\n", 144 | "\n", 145 | "nc = len(df)\n", 146 | "df['class'] = np.zeros(nc) - 1\n", 147 | "df['class'][car0] = np.zeros(nc)\n", 148 | "df['class'][car1] = np.zeros(nc)\n", 149 | "df['class'][bus0] = np.ones(nc)\n", 150 | "df['class'][bus1] = np.ones(nc)" 151 | ] 152 | }, 153 | { 154 | "cell_type": "code", 155 | "execution_count": 42, 156 | "metadata": { 157 | "collapsed": true 158 | }, 159 | "outputs": [], 160 | "source": [ 161 | "# separate into quarters for train and validation\n", 162 | "q1 = df[car0]\n", 163 | "q2 = df[car1]\n", 164 | "q3 = df[bus0]\n", 165 | "q4 = df[bus1]\n", 166 | "traindf = pd.concat([q2, q4])\n", 167 | "validationdf = pd.concat([q1, q3])" 168 | ] 169 | }, 170 | { 171 | "cell_type": "code", 172 | "execution_count": 43, 173 | "metadata": { 174 | "collapsed": false 175 | }, 176 | "outputs": [ 177 | { 178 | "name": "stdout", 179 | "output_type": "stream", 180 | "text": [ 181 | "0\n", 182 | "0\n" 183 | ] 184 | } 185 | ], 186 | "source": [ 187 | "# check for NaNs in the dataframes\n", 188 | "print(traindf.isnull().sum().sum())\n", 189 | "print(validationdf.isnull().sum().sum())" 190 | ] 191 | }, 192 | { 193 | "cell_type": "code", 194 | "execution_count": 44, 195 | "metadata": { 196 | "collapsed": true 197 | }, 198 | "outputs": [], 199 | "source": [ 200 | "# drop NaNs\n", 201 | "traindf = traindf.dropna()\n", 202 | "validationdf = validationdf.dropna()" 203 | ] 204 | }, 205 | { 206 | "cell_type": "code", 207 | "execution_count": 45, 208 | "metadata": { 209 | "collapsed": true 210 | }, 211 | "outputs": [], 212 | "source": [ 213 | "# Make the training and validation sets\n", 214 | "X_train = traindf[features].values\n", 215 | "y_train = traindf['class'].values\n", 216 | "X_test = validationdf[features].values\n", 217 | "y_test = validationdf['class'].values" 218 | ] 219 | }, 220 | { 221 | "cell_type": "code", 222 | "execution_count": 46, 223 | "metadata": { 224 | "collapsed": false 225 | }, 226 | "outputs": [], 227 | "source": [ 228 | "# train a random forest\n", 229 | "clf = RandomForestClassifier(n_estimators=200)" 230 | ] 231 | }, 232 | { 233 | "cell_type": "code", 234 | "execution_count": 47, 235 | "metadata": { 236 | "collapsed": false 237 | }, 238 | "outputs": [ 239 | { 240 | "name": "stdout", 241 | "output_type": "stream", 242 | "text": [ 243 | "(array([ 0.67006308, 0.63221737, 0.60436893, 0.68479845, 0.85769791]), 0.68982914717583199, 0.088564657518697895)\n" 244 | ] 245 | } 246 | ], 247 | "source": [ 248 | "# get the 5-fold cross-validation score\n", 249 | "scores = cross_val_score(clf, X_train, y_train, cv=5)\n", 250 | "print(scores, scores.mean(), scores.std())" 251 | ] 252 | }, 253 | { 254 | "cell_type": "code", 255 | "execution_count": 48, 256 | "metadata": { 257 | "collapsed": true 258 | }, 259 | "outputs": [], 260 | "source": [ 261 | "# apply model to test set\n", 262 | "clf.fit(X_train, y_train)\n", 263 | "predict_y = clf.predict(X_test)" 264 | ] 265 | }, 266 | { 267 | "cell_type": "code", 268 | "execution_count": 49, 269 | "metadata": { 270 | "collapsed": false 271 | }, 272 | "outputs": [ 273 | { 274 | "name": "stdout", 275 | "output_type": "stream", 276 | "text": [ 277 | "Accuracy score on test set: 0.654\n" 278 | ] 279 | } 280 | ], 281 | "source": [ 282 | "# obtain accuracy score\n", 283 | "testscore = accuracy_score(y_test, predict_y)\n", 284 | "print(\"Accuracy score on test set: %6.3f\" % testscore)" 285 | ] 286 | }, 287 | { 288 | "cell_type": "markdown", 289 | "metadata": {}, 290 | "source": [ 291 | "#### We're not overfitting the data, but we're also not really predicting the vehicle class very well, since we're only right about 65-70% of the time with any prediction we make." 292 | ] 293 | }, 294 | { 295 | "cell_type": "code", 296 | "execution_count": 50, 297 | "metadata": { 298 | "collapsed": false 299 | }, 300 | "outputs": [ 301 | { 302 | "name": "stdout", 303 | "output_type": "stream", 304 | "text": [ 305 | "userAccelerationX: 0.0022\n", 306 | "userAccelerationY: 0.0013\n", 307 | "userAccelerationZ: 0.0016\n", 308 | "gyroscopeX: 0.0033\n", 309 | "gyroscopeY: 0.0050\n", 310 | "gyroscopeZ: 0.0416\n", 311 | "userAccelerationXsm: 0.0141\n", 312 | "userAccelerationX2sm: 0.0390\n", 313 | "userAccelerationYsm: 0.0071\n", 314 | "userAccelerationY2sm: 0.0619\n", 315 | "userAccelerationZsm: 0.0036\n", 316 | "userAccelerationZ2sm: 0.0508\n", 317 | "gyroscopeXsm: 0.0117\n", 318 | "gyroscopeX2sm: 0.0495\n", 319 | "gyroscopeYsm: 0.0384\n", 320 | "gyroscopeY2sm: 0.0840\n", 321 | "gyroscopeZsm: 0.1003\n", 322 | "gyroscopeZ2sm: 0.1531\n", 323 | "userAccelerationXjerk: 0.0012\n", 324 | "userAccelerationYjerk: 0.0008\n", 325 | "userAccelerationZjerk: 0.0019\n", 326 | "gyroscopeXjerk: 0.0018\n", 327 | "gyroscopeYjerk: 0.0015\n", 328 | "gyroscopeZjerk: 0.0220\n", 329 | "userAccelerationXsmjerk: 0.0018\n", 330 | "userAccelerationX2smjerk: 0.0209\n", 331 | "userAccelerationYsmjerk: 0.0019\n", 332 | "userAccelerationY2smjerk: 0.0550\n", 333 | "userAccelerationZsmjerk: 0.0016\n", 334 | "userAccelerationZ2smjerk: 0.0227\n", 335 | "gyroscopeXsmjerk: 0.0089\n", 336 | "gyroscopeX2smjerk: 0.0329\n", 337 | "gyroscopeYsmjerk: 0.0111\n", 338 | "gyroscopeY2smjerk: 0.0609\n", 339 | "gyroscopeZsmjerk: 0.0299\n", 340 | "gyroscopeZ2smjerk: 0.0545\n" 341 | ] 342 | } 343 | ], 344 | "source": [ 345 | "# Inspect feature importances\n", 346 | "for i, ifeature in enumerate(features):\n", 347 | " print(ifeature + ': %6.4f' % clf.feature_importances_[i])" 348 | ] 349 | }, 350 | { 351 | "cell_type": "markdown", 352 | "metadata": {}, 353 | "source": [ 354 | "#### The smoothed gyroscopeZ data is the most useful feature." 355 | ] 356 | }, 357 | { 358 | "cell_type": "code", 359 | "execution_count": 60, 360 | "metadata": { 361 | "collapsed": false 362 | }, 363 | "outputs": [ 364 | { 365 | "data": { 366 | "text/plain": [ 367 | "" 368 | ] 369 | }, 370 | "execution_count": 60, 371 | "metadata": {}, 372 | "output_type": "execute_result" 373 | }, 374 | { 375 | "data": { 376 | "image/png": "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\n", 377 | "text/plain": [ 378 | "" 379 | ] 380 | }, 381 | "metadata": {}, 382 | "output_type": "display_data" 383 | } 384 | ], 385 | "source": [ 386 | "# compare bus gyroscopeZ2sm and car gyroscopeZ2sm\n", 387 | "q1['gyroscopeZ2sm'].plot(color='blue', figsize=(12,6), kind='hist', bins=40, alpha=0.4) # car\n", 388 | "q3['gyroscopeZ2sm'].plot(color='green', kind='hist', bins=40, alpha=0.4) # bus" 389 | ] 390 | }, 391 | { 392 | "cell_type": "markdown", 393 | "metadata": {}, 394 | "source": [ 395 | "#### Reflecting on this further, it occurs to me that this methodology is identifying that the bus trip and the car trip followed different routes and had different numbers and types of turns. A better way to go might be to identify features for each turn (e.g., time to complete turn, average accelerometer and gyroscope signal during turn, etc.) and apply the random forest to those features." 396 | ] 397 | }, 398 | { 399 | "cell_type": "markdown", 400 | "metadata": {}, 401 | "source": [ 402 | "#### Another interesting avenue to pursue is features in Fourier space" 403 | ] 404 | }, 405 | { 406 | "cell_type": "code", 407 | "execution_count": 61, 408 | "metadata": { 409 | "collapsed": false 410 | }, 411 | "outputs": [], 412 | "source": [ 413 | "# Generate Fourier Transform of features\n", 414 | "fftfeatures = []\n", 415 | "for i in features:\n", 416 | " reals = np.real(np.fft.rfft(df[i]))\n", 417 | " imags = np.imag(np.fft.rfft(df[i]))\n", 418 | " complexs = [reals[0]]\n", 419 | " n = len(reals)\n", 420 | " if n % 2 == 0:\n", 421 | " complexs.append(imags[0])\n", 422 | " for j in range(1, n - 1):\n", 423 | " complexs.append(reals[j])\n", 424 | " complexs.append(imags[j])\n", 425 | " complexs.append(reals[j])\n", 426 | " df['f' + i] = complexs\n", 427 | " fftfeatures.append('f' + i)\n", 428 | "features.extend(fftfeatures)" 429 | ] 430 | }, 431 | { 432 | "cell_type": "code", 433 | "execution_count": 62, 434 | "metadata": { 435 | "collapsed": true 436 | }, 437 | "outputs": [], 438 | "source": [ 439 | "# Make the training and validation sets\n", 440 | "X_train = traindf[fftfeatures].values\n", 441 | "y_train = traindf['class'].values\n", 442 | "X_test = validationdf[fftfeatures].values\n", 443 | "y_test = validationdf['class'].values" 444 | ] 445 | }, 446 | { 447 | "cell_type": "code", 448 | "execution_count": 63, 449 | "metadata": { 450 | "collapsed": false 451 | }, 452 | "outputs": [], 453 | "source": [ 454 | "# train a random forest\n", 455 | "clf = RandomForestClassifier(n_estimators=200)" 456 | ] 457 | }, 458 | { 459 | "cell_type": "code", 460 | "execution_count": 64, 461 | "metadata": { 462 | "collapsed": false 463 | }, 464 | "outputs": [ 465 | { 466 | "name": "stdout", 467 | "output_type": "stream", 468 | "text": [ 469 | "(array([ 1., 1., 1., 1., 1.]), 1.0, 0.0)\n" 470 | ] 471 | } 472 | ], 473 | "source": [ 474 | "# get the 5-fold cross-validation score\n", 475 | "scores = cross_val_score(clf, X_train, y_train, cv=5)\n", 476 | "print(scores, scores.mean(), scores.std())" 477 | ] 478 | }, 479 | { 480 | "cell_type": "code", 481 | "execution_count": 65, 482 | "metadata": { 483 | "collapsed": true 484 | }, 485 | "outputs": [], 486 | "source": [ 487 | "# apply model to test set\n", 488 | "clf.fit(X_train, y_train)\n", 489 | "predict_y = clf.predict(X_test)" 490 | ] 491 | }, 492 | { 493 | "cell_type": "code", 494 | "execution_count": 66, 495 | "metadata": { 496 | "collapsed": false 497 | }, 498 | "outputs": [ 499 | { 500 | "name": "stdout", 501 | "output_type": "stream", 502 | "text": [ 503 | "Accuracy score on test set: 0.868\n" 504 | ] 505 | } 506 | ], 507 | "source": [ 508 | "# obtain accuracy score\n", 509 | "testscore = accuracy_score(y_test, predict_y)\n", 510 | "print(\"Accuracy score on test set: %6.3f\" % testscore)" 511 | ] 512 | }, 513 | { 514 | "cell_type": "markdown", 515 | "metadata": {}, 516 | "source": [ 517 | "#### Much better accuracy on the test set: 87%. We are definitely overfitting here, since we got 100% accuracy on the training set. We are also probably suffering from the same problem using the time series data, where the classifier learns to classify based on the nature of the route, not the nature of the ride." 518 | ] 519 | }, 520 | { 521 | "cell_type": "code", 522 | "execution_count": 68, 523 | "metadata": { 524 | "collapsed": false 525 | }, 526 | "outputs": [ 527 | { 528 | "name": "stdout", 529 | "output_type": "stream", 530 | "text": [ 531 | "fuserAccelerationX: 0.0003\n", 532 | "fuserAccelerationY: 0.0004\n", 533 | "fuserAccelerationZ: 0.0000\n", 534 | "fgyroscopeX: 0.0004\n", 535 | "fgyroscopeY: 0.0002\n", 536 | "fgyroscopeZ: 0.0008\n", 537 | "fuserAccelerationXsm: 0.0455\n", 538 | "fuserAccelerationX2sm: 0.0199\n", 539 | "fuserAccelerationYsm: 0.1113\n", 540 | "fuserAccelerationY2sm: 0.0253\n", 541 | "fuserAccelerationZsm: 0.0360\n", 542 | "fuserAccelerationZ2sm: 0.0183\n", 543 | "fgyroscopeXsm: 0.0570\n", 544 | "fgyroscopeX2sm: 0.0431\n", 545 | "fgyroscopeYsm: 0.0558\n", 546 | "fgyroscopeY2sm: 0.0355\n", 547 | "fgyroscopeZsm: 0.0509\n", 548 | "fgyroscopeZ2sm: 0.0615\n", 549 | "fuserAccelerationXjerk: 0.0004\n", 550 | "fuserAccelerationYjerk: 0.0002\n", 551 | "fuserAccelerationZjerk: 0.0002\n", 552 | "fgyroscopeXjerk: 0.0006\n", 553 | "fgyroscopeYjerk: 0.0004\n", 554 | "fgyroscopeZjerk: 0.0002\n", 555 | "fuserAccelerationXsmjerk: 0.0381\n", 556 | "fuserAccelerationX2smjerk: 0.0000\n", 557 | "fuserAccelerationYsmjerk: 0.1315\n", 558 | "fuserAccelerationY2smjerk: 0.0001\n", 559 | "fuserAccelerationZsmjerk: 0.0069\n", 560 | "fuserAccelerationZ2smjerk: 0.0000\n", 561 | "fgyroscopeXsmjerk: 0.0480\n", 562 | "fgyroscopeX2smjerk: 0.0001\n", 563 | "fgyroscopeYsmjerk: 0.0607\n", 564 | "fgyroscopeY2smjerk: 0.0001\n", 565 | "fgyroscopeZsmjerk: 0.1500\n", 566 | "fgyroscopeZ2smjerk: 0.0002\n" 567 | ] 568 | } 569 | ], 570 | "source": [ 571 | "# Inspect feature importances\n", 572 | "for i, ifeature in enumerate(fftfeatures):\n", 573 | " print(ifeature + ': %6.4f' % clf.feature_importances_[i])" 574 | ] 575 | }, 576 | { 577 | "cell_type": "markdown", 578 | "metadata": {}, 579 | "source": [ 580 | "#### Interesting that the accelerometer signal is more important here. This could be an indication that training in Fourier space helps mitigate the route-based issues that we encountered when using the time series data." 581 | ] 582 | }, 583 | { 584 | "cell_type": "code", 585 | "execution_count": null, 586 | "metadata": { 587 | "collapsed": true 588 | }, 589 | "outputs": [], 590 | "source": [] 591 | } 592 | ], 593 | "metadata": { 594 | "kernelspec": { 595 | "display_name": "Python 2", 596 | "language": "python", 597 | "name": "python2" 598 | }, 599 | "language_info": { 600 | "codemirror_mode": { 601 | "name": "ipython", 602 | "version": 2 603 | }, 604 | "file_extension": ".py", 605 | "mimetype": "text/x-python", 606 | "name": "python", 607 | "nbconvert_exporter": "python", 608 | "pygments_lexer": "ipython2", 609 | "version": "2.7.10" 610 | } 611 | }, 612 | "nbformat": 4, 613 | "nbformat_minor": 0 614 | } 615 | -------------------------------------------------------------------------------- /Code/learnposition.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | Take a processed dataframe, generate features, add classification label, make 4 | validation set, predict class. 5 | 6 | """ 7 | 8 | import pandas as pd 9 | from scipy.ndimage import gaussian_filter 10 | from sklearn.ensemble import RandomForestClassifier 11 | from sklearn.cross_validation import cross_val_score 12 | from sklearn.metrics import accuracy_score 13 | import numpy as np 14 | 15 | 16 | df = pd.read_csv('shaneiphone_exp2_processed.csv', index_col='DateTime') 17 | 18 | # We are allowed to use only userAcceleration and gyroscope data 19 | xyz = ['X', 'Y', 'Z'] 20 | measures = ['userAcceleration', 'gyroscope'] 21 | features = [i + j for i in measures for j in xyz] 22 | 23 | # Generate Gaussian smoothed features 24 | smoothfeatures = [] 25 | for i in features: 26 | df[i + 'sm'] = gaussian_filter(df[i], 3) 27 | df[i + '2sm'] = gaussian_filter(df[i], 50) 28 | smoothfeatures.append(i + 'sm') 29 | smoothfeatures.append(i + '2sm') 30 | features.extend(smoothfeatures) 31 | 32 | # Generate Fourier Transform of features 33 | fftfeatures = [] 34 | for i in features: 35 | reals = np.real(np.fft.rfft(df[i])) 36 | imags = np.imag(np.fft.rfft(df[i])) 37 | complexs = [reals[0]] 38 | for j in range(1, reals.size - 1): 39 | complexs.append(reals[j]) 40 | complexs.append(imags[j]) 41 | complexs.append(reals[j]) 42 | n = len(reals) 43 | if n % 2 != 0: 44 | complexs.append(imags[j]) 45 | df['f' + i] = complexs 46 | fftfeatures.append('f' + i) 47 | features.extend(fftfeatures) 48 | 49 | df = df[features] 50 | 51 | # assign class labels 52 | class0 = (df.index > '2015-08-25 14:35:00') & \ 53 | (df.index < '2015-08-25 14:42:00') 54 | 55 | class1 = (df.index > '2015-08-25 14:43:00') & \ 56 | (df.index < '2015-08-25 14:48:00') 57 | 58 | df['class'] = -1 59 | df['class'][class0] = 0 60 | df['class'][class1] = 1 61 | 62 | # remove the unclassified portion of the data 63 | classed = df['class'] != -1 64 | df = df[classed] 65 | 66 | # separate into quarters for train and validation 67 | q1 = df[(df.index <= '2015-08-25 14:38:30') & 68 | (df.index > '2015-08-25 14:33:00')] 69 | q2 = df[(df.index > '2015-08-25 14:38:30') & 70 | (df.index <= '2015-08-25 14:42:00')] 71 | q3 = df[(df.index > '2015-08-25 14:43:00') & 72 | (df.index <= '2015-08-25 14:45:30')] 73 | q4 = df[(df.index > '2015-08-25 14:45:30') & 74 | (df.index <= '2015-08-25 14:48:00')] 75 | traindf = pd.concat([q1, q3]) 76 | validationdf = pd.concat([q2, q4]) 77 | 78 | # drop NaNs 79 | traindf = traindf.dropna() 80 | validationdf = validationdf.dropna() 81 | X_train = traindf[traindf.columns[0:-1]].values 82 | Y_train = traindf[traindf.columns[-1]].values 83 | X_test = validationdf[validationdf.columns[0:-1]].values 84 | Y_test = validationdf[validationdf.columns[-1]].values 85 | 86 | # train a random forest 87 | clf = RandomForestClassifier(n_estimators=100) 88 | clf.fit(X_train, Y_train) 89 | scores = cross_val_score(clf, X_train, Y_train, cv=5) 90 | Y_test_RFC = clf.predict(X_test) 91 | 92 | print("Results from cross-validation on training set:") 93 | print(scores, scores.mean(), scores.std()) 94 | 95 | testscore = accuracy_score(Y_test, Y_test_RFC) 96 | 97 | print("Accuracy score on test set: %6.3f" % testscore) 98 | 99 | print("Feature importances: ") 100 | for i, ifeature in enumerate(features): 101 | print(ifeature + ': %6.4f' % clf.feature_importances_[i]) 102 | 103 | import pdb; pdb.set_trace() 104 | -------------------------------------------------------------------------------- /Code/learnvehicle.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | Take a processed dataframe, generate features, add classification label, make 4 | validation set, predict class. 5 | 6 | """ 7 | 8 | import pandas as pd 9 | from scipy.ndimage import gaussian_filter 10 | from sklearn.ensemble import RandomForestClassifier 11 | from sklearn.cross_validation import cross_val_score 12 | from sklearn.metrics import accuracy_score 13 | import numpy as np 14 | 15 | 16 | dfcar = pd.read_csv('shaneiphone_exp2_processed.csv', index_col='DateTime') 17 | dfbus = pd.read_csv('shanebus20150827_processed.csv', index_col='DateTime') 18 | 19 | df = pd.concat([dfcar, dfbus]) 20 | 21 | # We are allowed to use only userAcceleration and gyroscope data 22 | xyz = ['X', 'Y', 'Z'] 23 | measures = ['userAcceleration', 'gyroscope'] 24 | features = [i + j for i in measures for j in xyz] 25 | 26 | # Generate Gaussian smoothed features 27 | smoothfeatures = [] 28 | for i in features: 29 | df[i + 'sm'] = gaussian_filter(df[i], 3) 30 | df[i + '2sm'] = gaussian_filter(df[i], 100) 31 | smoothfeatures.append(i + 'sm') 32 | smoothfeatures.append(i + '2sm') 33 | features.extend(smoothfeatures) 34 | 35 | # Generate Jerk signal 36 | jerkfeatures = [] 37 | for i in features: 38 | diffsignal = np.diff(df[i]) 39 | df[i + 'jerk'] = np.append(0, diffsignal) 40 | jerkfeatures.append(i + 'jerk') 41 | features.extend(jerkfeatures) 42 | 43 | # Generate Fourier Transform of features 44 | fftfeatures = [] 45 | for i in features: 46 | reals = np.real(np.fft.rfft(df[i])) 47 | imags = np.imag(np.fft.rfft(df[i])) 48 | complexs = [reals[0]] 49 | for j in range(1, reals.size - 1): 50 | complexs.append(reals[j]) 51 | complexs.append(imags[j]) 52 | complexs.append(reals[j]) 53 | df['f' + i] = complexs 54 | fftfeatures.append('f' + i) 55 | features.extend(fftfeatures) 56 | print(features) 57 | 58 | df = df[features] 59 | 60 | # assign class labels 61 | car0 = (df.index > '2015-08-25 14:35:00') & \ 62 | (df.index <= '2015-08-25 14:42:00') 63 | 64 | car1 = (df.index > '2015-08-25 14:43:00') & \ 65 | (df.index <= '2015-08-25 14:48:00') 66 | 67 | bus0 = (df.index > '2015-08-27 10:10:00') & \ 68 | (df.index <= '2015-08-27 10:15:00') 69 | bus1 = (df.index > '2015-08-27 10:15:00') & \ 70 | (df.index <= '2015-08-27 10:20:00') 71 | 72 | nc = len(df) 73 | df['class'] = np.zeros(nc) - 1 74 | df['class'][car0] = np.zeros(nc) 75 | df['class'][car1] = 0 76 | df['class'][bus0] = 1 77 | df['class'][bus1] = 1 78 | 79 | ## remove the unclassified portion of the data 80 | #classed = df['class'] != -1 81 | #df = df[classed] 82 | 83 | # separate into quarters for train and validation 84 | q1 = df[car0] 85 | q2 = df[car1] 86 | q3 = df[bus0] 87 | q4 = df[bus1] 88 | traindf = pd.concat([q2, q4]) 89 | validationdf = pd.concat([q1, q3]) 90 | 91 | # drop NaNs 92 | traindf = traindf.dropna() 93 | validationdf = validationdf.dropna() 94 | X_train = traindf[traindf.columns[0:-1]].values 95 | Y_train = traindf[traindf.columns[-1]].values 96 | X_test = validationdf[validationdf.columns[0:-1]].values 97 | Y_test = validationdf[validationdf.columns[-1]].values 98 | 99 | # train a random forest 100 | print("Beginning random forest classification") 101 | clf = RandomForestClassifier(n_estimators=1000) 102 | clf.fit(X_train, Y_train) 103 | scores = cross_val_score(clf, X_train, Y_train, cv=5) 104 | Y_test_RFC = clf.predict(X_test) 105 | 106 | print("Results from cross-validation on training set:") 107 | print(scores, scores.mean(), scores.std()) 108 | 109 | testscore = accuracy_score(Y_test, Y_test_RFC) 110 | 111 | print("Accuracy score on test set: %6.3f" % testscore) 112 | 113 | print("Feature importances:") 114 | print(clf.feature_importances_) 115 | 116 | import pdb; pdb.set_trace() 117 | from keras.models import Sequential 118 | from keras.layers.core import Dense, Dropout, Activation 119 | #from keras.layers.embeddings import Embedding 120 | #from keras.layers.recurrent import LSTM 121 | from keras.callbacks import ModelCheckpoint 122 | 123 | checkpointer = ModelCheckpoint(filepath="testnn.hdf5", verbose=1, save_best_only=True) 124 | model = Sequential() 125 | # Add a mask_zero=True to the Embedding connstructor if 0 is a left-padding value in your data 126 | max_features = 200000 127 | model.add(Dense(X_train.shape[1], 256)) 128 | model.add(Activation('relu')) 129 | #model.add(Embedding(X_train.shape[1], 256)) 130 | #model.add(LSTM(256, 128, activation='sigmoid', inner_activation='hard_sigmoid')) 131 | #model.add(Dropout(0.5)) 132 | model.add(Dense(256, 128)) 133 | model.add(Activation('relu')) 134 | model.add(Dense(128, 1)) 135 | model.add(Activation('sigmoid')) 136 | 137 | model.compile(loss='binary_crossentropy', optimizer='adam', class_mode='binary') 138 | 139 | 140 | call_back = model.fit(X_train, Y_train, batch_size=256, nb_epoch=200, 141 | show_accuracy=True, validation_data=[X_test, Y_test], 142 | callbacks=[checkpointer]) 143 | 144 | #score = model.evaluate(X_test, Y_test, batch_size=256) 145 | 146 | 147 | # In[430]: 148 | 149 | #plt.plot(model.predict(X_test)) 150 | import pdb; pdb.set_trace() 151 | -------------------------------------------------------------------------------- /Code/quatrotate.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | Rotate a 3D vector using the axis-angle method (quaternions). 4 | 5 | """ 6 | 7 | import numpy as np 8 | 9 | 10 | def normalize(v, tolerance=0.00001): 11 | mag2 = sum(n * n for n in v) 12 | if abs(mag2 - 1.0) > tolerance: 13 | mag = np.sqrt(mag2) 14 | v = tuple(n / mag for n in v) 15 | return v 16 | 17 | def q_mult(q1, q2): 18 | w1, x1, y1, z1 = q1 19 | w2, x2, y2, z2 = q2 20 | w = w1 * w2 - x1 * x2 - y1 * y2 - z1 * z2 21 | x = w1 * x2 + x1 * w2 + y1 * z2 - z1 * y2 22 | y = w1 * y2 + y1 * w2 + z1 * x2 - x1 * z2 23 | z = w1 * z2 + z1 * w2 + x1 * y2 - y1 * x2 24 | return w, x, y, z 25 | 26 | def q_conjugate(q): 27 | w, x, y, z = q 28 | return (w, -x, -y, -z) 29 | 30 | def qv_mult(q1, v1): 31 | q2 = (0.0,) + v1 32 | return q_mult(q_mult(q1, q2), q_conjugate(q1))[1:] 33 | 34 | def axisangle_to_q(v, theta): 35 | v = normalize(v) 36 | x, y, z = v 37 | theta /= 2 38 | w = np.cos(theta) 39 | x = x * np.sin(theta) 40 | y = y * np.sin(theta) 41 | z = z * np.sin(theta) 42 | return w, x, y, z 43 | 44 | def q_to_axisangle(q): 45 | w, v = q[0], q[1:] 46 | theta = np.acos(w) * 2.0 47 | return normalize(v), theta 48 | -------------------------------------------------------------------------------- /Code/sensordata.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: iso-8859-15 -*- 3 | 4 | import pandas as pd 5 | import numpy as np 6 | from quatrotate import qv_mult 7 | 8 | 9 | class Load: 10 | 11 | """ 12 | 13 | Process raw smartphone sensor data into a common format. 14 | 15 | Input 16 | dfloc: location on disk of csv file with raw smartphone data 17 | 18 | Output 19 | ndfloc: location on disk of new csv file with processed smartphone data 20 | 21 | """ 22 | 23 | def __init__(self, dfloc): 24 | self.dfloc = dfloc 25 | self.process() 26 | 27 | def process(self): 28 | 29 | print("Reading csv file into Pandas dataframe") 30 | self.df = pd.read_csv(self.dfloc) 31 | 32 | # SensorLog app for iPhone 33 | if 'motionUserAccelerationX' in self.df.columns: 34 | self.app = 'SensorLog' 35 | 36 | # make a new column with date and time and set it as index 37 | self.df['DateTime'] = pd.DatetimeIndex(self.df['loggingTime']) 38 | self.df = self.df.set_index('DateTime') 39 | 40 | # drop unneeded columns 41 | unneeded = ['loggingTime', 'loggingSample', 42 | 'identifierForVendor', 'deviceID', 43 | 'locationTimestamp_since1970', 'locationVerticalAccuracy', 44 | 'locationHorizontalAccuracy', 'locationFloor', 45 | 'locationHeadingTimestamp_since1970', 46 | 'locationHeadingAccuracy', 47 | 'accelerometerTimestamp_sinceReboot', 48 | 'gyroTimestamp_sinceReboot', 'motionTimestamp_sinceReboot', 49 | 'motionMagneticFieldCalibrationAccuracy', 50 | 'activityTimestamp_sinceReboot', 'activity', 51 | 'activityActivityConfidence', 'activityActivityStartDate', 52 | 'pedometerStartDate', 'pedometerNumberofSteps', 53 | 'pedometerDistance', 'pedometerFloorAscended', 54 | 'pedometerFloorDescended', 'pedometerEndDate', 'IP_en0', 55 | 'IP_pdp_ip0', 'deviceOrientation', 'batteryState', 56 | 'batteryLevel', 'state', 'locationCourse', 57 | 'locationHeadingX', 'locationHeadingY', 'locationHeadingZ', 58 | 'locationTrueHeading', 'locationMagneticHeading', 59 | 'motionRotationRateX', 'motionRotationRateY', 60 | 'motionRotationRateZ', 'motionAttitudeReferenceFrame'] 61 | self.df = self.df.drop(unneeded, axis=1) 62 | 63 | # rename remaining columns to standard system 64 | # assuming orientationZ = motionYaw, orientationY = motionRoll 65 | self.df.columns = ['latitude', 'longitude', 'altitude', 'speed', 66 | 'accelerometerX', 'accelerometerY', 'accelerometerZ', 67 | 'gyroscopeX', 'gyroscopeY', 'gyroscopeZ', 68 | 'orientationZ', 'orientationY', 'orientationX', 69 | 'userAccelerationX', 'userAccelerationY', 70 | 'userAccelerationZ', 71 | 'quaternionX', 'quaternionY', 'quaternionZ', 'quaternionW', 72 | 'gravityX', 'gravityY', 'gravityZ', 73 | 'magneticFieldX', 'magneticFieldY', 'magneticFieldZ'] 74 | 75 | # AndroSensor app for Android 76 | if 'GYROSCOPE X (rad/s)' in self.df.columns: 77 | self.app = 'AndroSensor' 78 | 79 | # modify the android date column to be SS.SSS (vs. SS:SSS) 80 | if self.df['YYYY-MO-DD HH-MI-SS_SSS'][0][-4] == ':': 81 | print("Rewriting date column") 82 | reviseddates = [] 83 | for i in range(len(self.df)): 84 | start = self.df['YYYY-MO-DD HH-MI-SS_SSS'][i][0:-4] 85 | mid = '.' 86 | finish = self.df['YYYY-MO-DD HH-MI-SS_SSS'][i][-3:] 87 | replaced = start + mid + finish 88 | reviseddates.append(replaced) 89 | self.df['YYYY-MO-DD HH-MI-SS_SSS'] = reviseddates 90 | 91 | print("Reformatting date column to DatetimeIndex") 92 | # make a new column with date and time and set it as index 93 | self.df['DateTime'] = \ 94 | pd.DatetimeIndex(self.df['YYYY-MO-DD HH-MI-SS_SSS']) 95 | self.df = self.df.set_index('DateTime') 96 | 97 | print("Dropping unnecessary columns") 98 | # drop unneeded columns 99 | unneeded = ['LIGHT (lux)', 'PROXIMITY (i)', 100 | 'ATMOSPHERIC PRESSURE (hPa)', 'SOUND LEVEL (dB)', 101 | 'LOCATION Altitude-google ( m)', 102 | 'LOCATION Altitude-atmospheric pressure ( m)', 103 | 'LOCATION Accuracy ( m)', 'LOCATION ORIENTATION (°)', 104 | 'Satellites in range', 'Time since start in ms ', 105 | 'YYYY-MO-DD HH-MI-SS_SSS'] 106 | self.df = self.df.drop(unneeded, axis=1) 107 | 108 | print("Renaming remaining columns") 109 | # rename remaining columns to standard system 110 | self.df = self.df.rename(columns={\ 111 | 'ACCELEROMETER X (m/s²)':'accelerometerX', 112 | 'ACCELEROMETER Y (m/s²)':'accelerometerY', 113 | 'ACCELEROMETER Z (m/s²)':'accelerometerZ', 114 | 'GRAVITY X (m/s²)':'gravityX', 115 | 'GRAVITY Y (m/s²)':'gravityY', 116 | 'GRAVITY Z (m/s²)':'gravityZ', 117 | 'LINEAR ACCELERATION X (m/s²)':'userAccelerationX', 118 | 'LINEAR ACCELERATION Y (m/s²)':'userAccelerationY', 119 | 'LINEAR ACCELERATION Z (m/s²)':'userAccelerationZ', 120 | 'GYROSCOPE X (rad/s)':'gyroscopeX', 121 | 'GYROSCOPE Y (rad/s)':'gyroscopeY', 122 | 'GYROSCOPE Z (rad/s)':'gyroscopeZ', 123 | 'MAGNETIC FIELD X (μT)':'magneticFieldX', 124 | 'MAGNETIC FIELD Y (μT)':'magneticFieldY', 125 | 'MAGNETIC FIELD Z (μT)':'magneticFieldZ', 126 | 'ORIENTATION Z (azimuth °)':'orientationZ', 127 | 'ORIENTATION X (pitch °)':'orientationX', 128 | 'ORIENTATION Y (roll °)':'orientationY', 129 | 'LOCATION Latitude : ':'latitude', 130 | 'LOCATION Longitude : ':'longitude', 131 | 'LOCATION Altitude ( m)':'altitude', 132 | 'LOCATION Speed ( Kmh)':'speed'}) 133 | 134 | print(self.df.columns) 135 | print("Computing quaternion") 136 | # compute the Quaternion 4-vector and add it to the dataframe 137 | qw, qx, qy, qz = self.getQuat() 138 | self.df['quaternionW'] = qw 139 | self.df['quaternionX'] = qx 140 | self.df['quaternionY'] = qy 141 | self.df['quaternionZ'] = qz 142 | 143 | # rotate the XYZ measurements to a common reference frame 144 | print("Rotating to common reference frame...") 145 | self.rotate() 146 | 147 | # resample to 10 Hz (period = 100 ms) 148 | print("Resampling to 10 Hz") 149 | self.df.resample('100L') 150 | 151 | # write resulting dataframe to a new csv file 152 | self.write() 153 | 154 | 155 | def getQuat(self): 156 | 157 | """ Given 3 orientation angles, compute the quaternion. """ 158 | 159 | yaw = self.df['orientationZ'] / 2. * np.pi / 180 160 | roll = self.df['orientationX'] / 2. * np.pi / 180 161 | pitch = self.df['orientationY'] / 2. * np.pi / 180 162 | 163 | w = np.cos(roll) * np.cos(pitch) * np.cos(yaw) + \ 164 | np.sin(roll) * np.sin(pitch) * np.sin(yaw) 165 | 166 | x = np.sin(roll) * np.cos(pitch) * np.cos(yaw) - \ 167 | np.cos(roll) * np.sin(pitch) * np.sin(yaw) 168 | 169 | y = np.cos(roll) * np.sin(pitch) * np.cos(yaw) + \ 170 | np.sin(roll) * np.cos(pitch) * np.sin(yaw) 171 | 172 | z = np.cos(roll) * np.cos(pitch) * np.sin(yaw) - \ 173 | np.sin(roll) * np.sin(pitch) * np.cos(yaw) 174 | 175 | return w, x, y, z 176 | 177 | def rotate(self): 178 | 179 | """ Generate rdf, a rotated version of df where the z-axis is aligned 180 | with gravity. """ 181 | 182 | varlist = ['accelerometer', 'orientation', 'userAcceleration', 183 | 'gravity', 'magneticField', 'gyroscope'] 184 | 185 | quaternion = self.df[['quaternionW', 'quaternionX', 186 | 'quaternionY', 'quaternionZ']].values 187 | 188 | for ivar in varlist: 189 | print("..." + ivar) 190 | xyzlist = [ivar + 'X', ivar + 'Y', ivar + 'Z'] 191 | xyz = self.df[xyzlist].values 192 | xyz_rotated = getrot(quaternion, xyz) 193 | self.df[ivar + 'X'] = xyz_rotated[:, 0] 194 | self.df[ivar + 'Y'] = xyz_rotated[:, 1] 195 | self.df[ivar + 'Z'] = xyz_rotated[:, 2] 196 | 197 | def write(self): 198 | 199 | """ Write dataframe to disk. """ 200 | 201 | ndfloc = self.dfloc[0:-4] + '_processed.csv' 202 | print("Storing dataframe with modified date column to " + 203 | ndfloc) 204 | self.df.to_csv(ndfloc) 205 | 206 | def getrot(quatern, vector): 207 | rotatedvector = [] 208 | for i in range(vector.shape[0]): 209 | rotatedvector.append(qv_mult(tuple(quatern[i, :]), 210 | tuple(vector[i, :]))) 211 | return np.array(rotatedvector) 212 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Shane Bussmann 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sensor-fusion 2 | Use accelerometer and gyroscope data from smartphones to identify vehicle type (bus or car) and phone location (driver side or passenger side). 3 | 4 | 1. Extract gravity signal (see "Extract Gravity Signal" jupyter notebook) 5 | 2. Rotate XYZ signals to vehicle reference frame (see "Rotate Sensor Data to 6 | Vehicle Reference Frame" jupyter notebook) 7 | 3. Resample time series data to 10 Hz sampling rate (see "Resample Sensor 8 | Data to 10 Hz Sampling Rate" jupyter notebook) 9 | 4. Automate steps 1-3, modify columns to standard system, and save result to 10 | a new file (see "Process Smartphone Sensor Data" jupyter notebook) 11 | 12 | Two examples of use cases of this software: 13 | 14 | 1. Vehicle classification exercise: determine whether the smartphone is on a 15 | bus or in a car based on 5-10 minutes of sensor data (see "Vehicle 16 | Classification Exercise" jupyter notebook) 17 | 2. Phone position classification exercise: determine whether the smartphone 18 | is on the driver side or passenger side of a car based on 5-10 minutes of 19 | sensor data (see "Phone Position Classification Exercise" jupyter notebook) 20 | --------------------------------------------------------------------------------