├── README.pdf ├── images ├── pivot.png ├── change_kernel.png ├── exec_location.png └── exec_location2.png ├── environment.yml ├── tests ├── __init__.py └── test_dataframe.py ├── LICENSE ├── .gitignore ├── Test Notebook.ipynb ├── pandas_cub └── __init__.py ├── pandas_cub_final └── __init__.py ├── README.md └── data └── employee.csv /README.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tdpetrou/pandas_cub/HEAD/README.pdf -------------------------------------------------------------------------------- /images/pivot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tdpetrou/pandas_cub/HEAD/images/pivot.png -------------------------------------------------------------------------------- /images/change_kernel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tdpetrou/pandas_cub/HEAD/images/change_kernel.png -------------------------------------------------------------------------------- /images/exec_location.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tdpetrou/pandas_cub/HEAD/images/exec_location.png -------------------------------------------------------------------------------- /environment.yml: -------------------------------------------------------------------------------- 1 | name: pandas_cub 2 | dependencies: 3 | - python=3.6 4 | - pandas 5 | - jupyter 6 | - pytest -------------------------------------------------------------------------------- /images/exec_location2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tdpetrou/pandas_cub/HEAD/images/exec_location2.png -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | from numpy.testing import assert_array_equal, assert_allclose 2 | 3 | 4 | def assert_df_equals(df1, df2): 5 | assert df1.columns == df2.columns 6 | for values1, values2 in zip(df1._data.values(), df2._data.values()): 7 | kind = values1.dtype.kind 8 | if kind == 'f': 9 | assert_allclose(values1, values2) 10 | else: 11 | assert_array_equal(values1, values2) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2018, Ted Petrou 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | .DS_Store 10 | .idea 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | .hypothesis/ 50 | .pytest_cache/ 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | local_settings.py 59 | db.sqlite3 60 | 61 | # Flask stuff: 62 | instance/ 63 | .webassets-cache 64 | 65 | # Scrapy stuff: 66 | .scrapy 67 | 68 | # Sphinx documentation 69 | docs/_build/ 70 | 71 | # PyBuilder 72 | target/ 73 | 74 | # Jupyter Notebook 75 | .ipynb_checkpoints 76 | 77 | # pyenv 78 | .python-version 79 | 80 | # celery beat schedule file 81 | celerybeat-schedule 82 | 83 | # SageMath parsed files 84 | *.sage.py 85 | 86 | # Environments 87 | .env 88 | .venv 89 | env/ 90 | venv/ 91 | ENV/ 92 | env.bak/ 93 | venv.bak/ 94 | 95 | # Spyder project settings 96 | .spyderproject 97 | .spyproject 98 | 99 | # Rope project settings 100 | .ropeproject 101 | 102 | # mkdocs documentation 103 | /site 104 | 105 | # mypy 106 | .mypy_cache/ 107 | -------------------------------------------------------------------------------- /Test Notebook.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Test pandas_cub manually\n", 8 | "\n", 9 | "In this notebook, we will test pandas_cub manually." 10 | ] 11 | }, 12 | { 13 | "cell_type": "code", 14 | "execution_count": null, 15 | "metadata": {}, 16 | "outputs": [], 17 | "source": [ 18 | "import sys\n", 19 | "sys.executable" 20 | ] 21 | }, 22 | { 23 | "cell_type": "code", 24 | "execution_count": null, 25 | "metadata": {}, 26 | "outputs": [], 27 | "source": [ 28 | "%load_ext autoreload\n", 29 | "%autoreload 2" 30 | ] 31 | }, 32 | { 33 | "cell_type": "code", 34 | "execution_count": null, 35 | "metadata": {}, 36 | "outputs": [], 37 | "source": [ 38 | "import numpy as np\n", 39 | "import pandas_cub as pdc\n", 40 | "import pandas_cub_final as pdcf\n", 41 | "import pandas as pd" 42 | ] 43 | }, 44 | { 45 | "cell_type": "code", 46 | "execution_count": null, 47 | "metadata": {}, 48 | "outputs": [], 49 | "source": [ 50 | "name = np.array(['Penelope', 'Niko', 'Eleni'])\n", 51 | "state = np.array(['Texas', 'California', 'Texas'])\n", 52 | "height = np.array([3.6, 3.5, 5.2])\n", 53 | "school = np.array([True, False, True])\n", 54 | "weight = np.array([45, 40, 130])\n", 55 | "\n", 56 | "data = {'name': name, 'state': state, 'height': height, \n", 57 | " 'school': school, 'weight': weight}\n", 58 | "\n", 59 | "df = pdc.DataFrame(data)\n", 60 | "df_final = pdcf.DataFrame(data)\n", 61 | "df_pandas = pd.DataFrame(data)" 62 | ] 63 | }, 64 | { 65 | "cell_type": "code", 66 | "execution_count": null, 67 | "metadata": {}, 68 | "outputs": [], 69 | "source": [ 70 | "df" 71 | ] 72 | }, 73 | { 74 | "cell_type": "code", 75 | "execution_count": null, 76 | "metadata": {}, 77 | "outputs": [], 78 | "source": [ 79 | "df_final" 80 | ] 81 | }, 82 | { 83 | "cell_type": "code", 84 | "execution_count": null, 85 | "metadata": {}, 86 | "outputs": [], 87 | "source": [ 88 | "df_pandas" 89 | ] 90 | }, 91 | { 92 | "cell_type": "code", 93 | "execution_count": null, 94 | "metadata": {}, 95 | "outputs": [], 96 | "source": [] 97 | } 98 | ], 99 | "metadata": { 100 | "kernelspec": { 101 | "display_name": "Python 3", 102 | "language": "python", 103 | "name": "python3" 104 | }, 105 | "language_info": { 106 | "codemirror_mode": { 107 | "name": "ipython", 108 | "version": 3 109 | }, 110 | "file_extension": ".py", 111 | "mimetype": "text/x-python", 112 | "name": "python", 113 | "nbconvert_exporter": "python", 114 | "pygments_lexer": "ipython3", 115 | "version": "3.6.8" 116 | } 117 | }, 118 | "nbformat": 4, 119 | "nbformat_minor": 2 120 | } 121 | -------------------------------------------------------------------------------- /pandas_cub/__init__.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | __version__ = '0.0.1' 4 | 5 | 6 | class DataFrame: 7 | 8 | def __init__(self, data): 9 | """ 10 | A DataFrame holds two dimensional heterogeneous data. Create it by 11 | passing a dictionary of NumPy arrays to the values parameter 12 | 13 | Parameters 14 | ---------- 15 | data: dict 16 | A dictionary of strings mapped to NumPy arrays. The key will 17 | become the column name. 18 | """ 19 | # check for correct input types 20 | self._check_input_types(data) 21 | 22 | # check for equal array lengths 23 | self._check_array_lengths(data) 24 | 25 | # convert unicode arrays to object 26 | self._data = self._convert_unicode_to_object(data) 27 | 28 | # Allow for special methods for strings 29 | self.str = StringMethods(self) 30 | self._add_docs() 31 | 32 | def _check_input_types(self, data): 33 | pass 34 | 35 | def _check_array_lengths(self, data): 36 | pass 37 | 38 | def _convert_unicode_to_object(self, data): 39 | new_data = {} 40 | return new_data 41 | 42 | def __len__(self): 43 | """ 44 | Make the builtin len function work with our dataframe 45 | 46 | Returns 47 | ------- 48 | int: the number of rows in the dataframe 49 | """ 50 | pass 51 | 52 | @property 53 | def columns(self): 54 | """ 55 | _data holds column names mapped to arrays 56 | take advantage of internal ordering of dictionaries to 57 | put columns in correct order in list. Only works in 3.6+ 58 | 59 | Returns 60 | ------- 61 | list of column names 62 | """ 63 | pass 64 | 65 | @columns.setter 66 | def columns(self, columns): 67 | """ 68 | Must supply a list of columns as strings the same length 69 | as the current DataFrame 70 | 71 | Parameters 72 | ---------- 73 | columns: list of strings 74 | 75 | Returns 76 | ------- 77 | None 78 | """ 79 | pass 80 | 81 | @property 82 | def shape(self): 83 | """ 84 | Returns 85 | ------- 86 | two-item tuple of number of rows and columns 87 | """ 88 | pass 89 | 90 | def _repr_html_(self): 91 | """ 92 | Used to create a string of HTML to nicely display the DataFrame 93 | in a Jupyter Notebook. Different string formatting is used for 94 | different data types. 95 | 96 | The structure of the HTML is as follows: 97 | 98 | 99 | 100 | 101 | ... 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | ... 110 | 111 | 112 | ... 113 | 114 | 115 | 116 | ... 117 | 118 | 119 | 120 |
datadata
{i}datadata
{i}datadata
121 | """ 122 | pass 123 | 124 | @property 125 | def values(self): 126 | """ 127 | Returns 128 | ------- 129 | A single 2D NumPy array of the underlying data 130 | """ 131 | pass 132 | 133 | @property 134 | def dtypes(self): 135 | """ 136 | Returns 137 | ------- 138 | A two-column DataFrame of column names in one column and 139 | their data type in the other 140 | """ 141 | DTYPE_NAME = {'O': 'string', 'i': 'int', 'f': 'float', 'b': 'bool'} 142 | pass 143 | 144 | def __getitem__(self, item): 145 | """ 146 | Use the brackets operator to simultaneously select rows and columns 147 | A single string selects one column -> df['colname'] 148 | A list of strings selects multiple columns -> df[['colname1', 'colname2']] 149 | A one column DataFrame of booleans that filters rows -> df[df_bool] 150 | Row and column selection simultaneously -> df[rs, cs] 151 | where cs and rs can be integers, slices, or a list of integers 152 | rs can also be a one-column boolean DataFrame 153 | 154 | Returns 155 | ------- 156 | A subset of the original DataFrame 157 | """ 158 | pass 159 | 160 | def _getitem_tuple(self, item): 161 | # simultaneous selection of rows and cols -> df[rs, cs] 162 | pass 163 | 164 | def _ipython_key_completions_(self): 165 | # allows for tab completion when doing df['c 166 | pass 167 | 168 | def __setitem__(self, key, value): 169 | # adds a new column or a overwrites an old column 170 | pass 171 | 172 | def head(self, n=5): 173 | """ 174 | Return the first n rows 175 | 176 | Parameters 177 | ---------- 178 | n: int 179 | 180 | Returns 181 | ------- 182 | DataFrame 183 | """ 184 | pass 185 | 186 | def tail(self, n=5): 187 | """ 188 | Return the last n rows 189 | 190 | Parameters 191 | ---------- 192 | n: int 193 | 194 | Returns 195 | ------- 196 | DataFrame 197 | """ 198 | pass 199 | 200 | #### Aggregation Methods #### 201 | 202 | def min(self): 203 | return self._agg(np.min) 204 | 205 | def max(self): 206 | return self._agg(np.max) 207 | 208 | def mean(self): 209 | return self._agg(np.mean) 210 | 211 | def median(self): 212 | return self._agg(np.median) 213 | 214 | def sum(self): 215 | return self._agg(np.sum) 216 | 217 | def var(self): 218 | return self._agg(np.var) 219 | 220 | def std(self): 221 | return self._agg(np.std) 222 | 223 | def all(self): 224 | return self._agg(np.all) 225 | 226 | def any(self): 227 | return self._agg(np.any) 228 | 229 | def argmax(self): 230 | return self._agg(np.argmax) 231 | 232 | def argmin(self): 233 | return self._agg(np.argmin) 234 | 235 | def _agg(self, aggfunc): 236 | """ 237 | Generic aggregation function that applies the 238 | aggregation to each column 239 | 240 | Parameters 241 | ---------- 242 | aggfunc: str of the aggregation function name in NumPy 243 | 244 | Returns 245 | ------- 246 | A DataFrame 247 | """ 248 | pass 249 | 250 | def isna(self): 251 | """ 252 | Determines whether each value in the DataFrame is missing or not 253 | 254 | Returns 255 | ------- 256 | A DataFrame of booleans the same size as the calling DataFrame 257 | """ 258 | pass 259 | 260 | def count(self): 261 | """ 262 | Counts the number of non-missing values per column 263 | 264 | Returns 265 | ------- 266 | A DataFrame 267 | """ 268 | pass 269 | 270 | def unique(self): 271 | """ 272 | Finds the unique values of each column 273 | 274 | Returns 275 | ------- 276 | A list of one-column DataFrames 277 | """ 278 | pass 279 | 280 | def nunique(self): 281 | """ 282 | Find the number of unique values in each column 283 | 284 | Returns 285 | ------- 286 | A DataFrame 287 | """ 288 | pass 289 | 290 | def value_counts(self, normalize=False): 291 | """ 292 | Returns the frequency of each unique value for each column 293 | 294 | Parameters 295 | ---------- 296 | normalize: bool 297 | If True, returns the relative frequencies (percent) 298 | 299 | Returns 300 | ------- 301 | A list of DataFrames or a single DataFrame if one column 302 | """ 303 | pass 304 | 305 | def rename(self, columns): 306 | """ 307 | Renames columns in the DataFrame 308 | 309 | Parameters 310 | ---------- 311 | columns: dict 312 | A dictionary mapping the old column name to the new column name 313 | 314 | Returns 315 | ------- 316 | A DataFrame 317 | """ 318 | pass 319 | 320 | def drop(self, columns): 321 | """ 322 | Drops one or more columns from a DataFrame 323 | 324 | Parameters 325 | ---------- 326 | columns: str or list of strings 327 | 328 | Returns 329 | ------- 330 | A DataFrame 331 | """ 332 | pass 333 | 334 | #### Non-Aggregation Methods #### 335 | 336 | def abs(self): 337 | """ 338 | Takes the absolute value of each value in the DataFrame 339 | 340 | Returns 341 | ------- 342 | A DataFrame 343 | """ 344 | return self._non_agg(np.abs) 345 | 346 | def cummin(self): 347 | """ 348 | Finds cumulative minimum by column 349 | 350 | Returns 351 | ------- 352 | A DataFrame 353 | """ 354 | return self._non_agg(np.minimum.accumulate) 355 | 356 | def cummax(self): 357 | """ 358 | Finds cumulative maximum by column 359 | 360 | Returns 361 | ------- 362 | A DataFrame 363 | """ 364 | return self._non_agg(np.maximum.accumulate) 365 | 366 | def cumsum(self): 367 | """ 368 | Finds cumulative sum by column 369 | 370 | Returns 371 | ------- 372 | A DataFrame 373 | """ 374 | return self._non_agg(np.cumsum) 375 | 376 | def clip(self, lower=None, upper=None): 377 | """ 378 | All values less than lower will be set to lower 379 | All values greater than upper will be set to upper 380 | 381 | Parameters 382 | ---------- 383 | lower: number or None 384 | upper: number or None 385 | 386 | Returns 387 | ------- 388 | A DataFrame 389 | """ 390 | return self._non_agg(np.clip, a_min=lower, a_max=upper) 391 | 392 | def round(self, n): 393 | """ 394 | Rounds values to the nearest n decimals 395 | 396 | Returns 397 | ------- 398 | A DataFrame 399 | """ 400 | return self._non_agg(np.round, decimals=n) 401 | 402 | def copy(self): 403 | """ 404 | Copies the DataFrame 405 | 406 | Returns 407 | ------- 408 | A DataFrame 409 | """ 410 | return self._non_agg(np.copy) 411 | 412 | def _non_agg(self, funcname, **kwargs): 413 | """ 414 | Generic non-aggregation function 415 | 416 | Parameters 417 | ---------- 418 | funcname: numpy function 419 | kwargs: extra keyword arguments for certain functions 420 | 421 | Returns 422 | ------- 423 | A DataFrame 424 | """ 425 | pass 426 | 427 | def diff(self, n=1): 428 | """ 429 | Take the difference between the current value and 430 | the nth value above it. 431 | 432 | Parameters 433 | ---------- 434 | n: int 435 | 436 | Returns 437 | ------- 438 | A DataFrame 439 | """ 440 | def func(): 441 | pass 442 | return self._non_agg(func) 443 | 444 | def pct_change(self, n=1): 445 | """ 446 | Take the percentage difference between the current value and 447 | the nth value above it. 448 | 449 | Parameters 450 | ---------- 451 | n: int 452 | 453 | Returns 454 | ------- 455 | A DataFrame 456 | """ 457 | def func(): 458 | pass 459 | return self._non_agg(func) 460 | 461 | #### Arithmetic and Comparison Operators #### 462 | 463 | def __add__(self, other): 464 | return self._oper('__add__', other) 465 | 466 | def __radd__(self, other): 467 | return self._oper('__radd__', other) 468 | 469 | def __sub__(self, other): 470 | return self._oper('__sub__', other) 471 | 472 | def __rsub__(self, other): 473 | return self._oper('__rsub__', other) 474 | 475 | def __mul__(self, other): 476 | return self._oper('__mul__', other) 477 | 478 | def __rmul__(self, other): 479 | return self._oper('__rmul__', other) 480 | 481 | def __truediv__(self, other): 482 | return self._oper('__truediv__', other) 483 | 484 | def __rtruediv__(self, other): 485 | return self._oper('__rtruediv__', other) 486 | 487 | def __floordiv__(self, other): 488 | return self._oper('__floordiv__', other) 489 | 490 | def __rfloordiv__(self, other): 491 | return self._oper('__rfloordiv__', other) 492 | 493 | def __pow__(self, other): 494 | return self._oper('__pow__', other) 495 | 496 | def __rpow__(self, other): 497 | return self._oper('__rpow__', other) 498 | 499 | def __gt__(self, other): 500 | return self._oper('__gt__', other) 501 | 502 | def __lt__(self, other): 503 | return self._oper('__lt__', other) 504 | 505 | def __ge__(self, other): 506 | return self._oper('__ge__', other) 507 | 508 | def __le__(self, other): 509 | return self._oper('__le__', other) 510 | 511 | def __ne__(self, other): 512 | return self._oper('__ne__', other) 513 | 514 | def __eq__(self, other): 515 | return self._oper('__eq__', other) 516 | 517 | def _oper(self, op, other): 518 | """ 519 | Generic operator function 520 | 521 | Parameters 522 | ---------- 523 | op: str name of special method 524 | other: the other object being operated on 525 | 526 | Returns 527 | ------- 528 | A DataFrame 529 | """ 530 | pass 531 | 532 | def sort_values(self, by, asc=True): 533 | """ 534 | Sort the DataFrame by one or more values 535 | 536 | Parameters 537 | ---------- 538 | by: str or list of column names 539 | asc: boolean of sorting order 540 | 541 | Returns 542 | ------- 543 | A DataFrame 544 | """ 545 | pass 546 | 547 | def sample(self, n=None, frac=None, replace=False, seed=None): 548 | """ 549 | Randomly samples rows the DataFrame 550 | 551 | Parameters 552 | ---------- 553 | n: int 554 | number of rows to return 555 | frac: float 556 | Proportion of the data to sample 557 | replace: bool 558 | Whether or not to sample with replacement 559 | seed: int 560 | Seeds the random number generator 561 | 562 | Returns 563 | ------- 564 | A DataFrame 565 | """ 566 | pass 567 | 568 | def pivot_table(self, rows=None, columns=None, values=None, aggfunc=None): 569 | """ 570 | Creates a pivot table from one or two 'grouping' columns. 571 | 572 | Parameters 573 | ---------- 574 | rows: str of column name to group by 575 | Optional 576 | columns: str of column name to group by 577 | Optional 578 | values: str of column name to aggregate 579 | Required 580 | aggfunc: str of aggregation function 581 | 582 | Returns 583 | ------- 584 | A DataFrame 585 | """ 586 | pass 587 | 588 | def _add_docs(self): 589 | agg_names = ['min', 'max', 'mean', 'median', 'sum', 'var', 590 | 'std', 'any', 'all', 'argmax', 'argmin'] 591 | agg_doc = \ 592 | """ 593 | Find the {} of each column 594 | 595 | Returns 596 | ------- 597 | DataFrame 598 | """ 599 | for name in agg_names: 600 | getattr(DataFrame, name).__doc__ = agg_doc.format(name) 601 | 602 | 603 | class StringMethods: 604 | 605 | def __init__(self, df): 606 | self._df = df 607 | 608 | def capitalize(self, col): 609 | return self._str_method(str.capitalize, col) 610 | 611 | def center(self, col, width, fillchar=None): 612 | if fillchar is None: 613 | fillchar = ' ' 614 | return self._str_method(str.center, col, width, fillchar) 615 | 616 | def count(self, col, sub, start=None, stop=None): 617 | return self._str_method(str.count, col, sub, start, stop) 618 | 619 | def endswith(self, col, suffix, start=None, stop=None): 620 | return self._str_method(str.endswith, col, suffix, start, stop) 621 | 622 | def startswith(self, col, suffix, start=None, stop=None): 623 | return self._str_method(str.startswith, col, suffix, start, stop) 624 | 625 | def find(self, col, sub, start=None, stop=None): 626 | return self._str_method(str.find, col, sub, start, stop) 627 | 628 | def len(self, col): 629 | return self._str_method(str.__len__, col) 630 | 631 | def get(self, col, item): 632 | return self._str_method(str.__getitem__, col, item) 633 | 634 | def index(self, col, sub, start=None, stop=None): 635 | return self._str_method(str.index, col, sub, start, stop) 636 | 637 | def isalnum(self, col): 638 | return self._str_method(str.isalnum, col) 639 | 640 | def isalpha(self, col): 641 | return self._str_method(str.isalpha, col) 642 | 643 | def isdecimal(self, col): 644 | return self._str_method(str.isdecimal, col) 645 | 646 | def islower(self, col): 647 | return self._str_method(str.islower, col) 648 | 649 | def isnumeric(self, col): 650 | return self._str_method(str.isnumeric, col) 651 | 652 | def isspace(self, col): 653 | return self._str_method(str.isspace, col) 654 | 655 | def istitle(self, col): 656 | return self._str_method(str.istitle, col) 657 | 658 | def isupper(self, col): 659 | return self._str_method(str.isupper, col) 660 | 661 | def lstrip(self, col, chars): 662 | return self._str_method(str.lstrip, col, chars) 663 | 664 | def rstrip(self, col, chars): 665 | return self._str_method(str.rstrip, col, chars) 666 | 667 | def strip(self, col, chars): 668 | return self._str_method(str.strip, col, chars) 669 | 670 | def replace(self, col, old, new, count=None): 671 | if count is None: 672 | count = -1 673 | return self._str_method(str.replace, col, old, new, count) 674 | 675 | def swapcase(self, col): 676 | return self._str_method(str.swapcase, col) 677 | 678 | def title(self, col): 679 | return self._str_method(str.title, col) 680 | 681 | def lower(self, col): 682 | return self._str_method(str.lower, col) 683 | 684 | def upper(self, col): 685 | return self._str_method(str.upper, col) 686 | 687 | def zfill(self, col, width): 688 | return self._str_method(str.zfill, col, width) 689 | 690 | def encode(self, col, encoding='utf-8', errors='strict'): 691 | return self._str_method(str.encode, col, encoding, errors) 692 | 693 | def _str_method(self, method, col, *args): 694 | pass 695 | 696 | 697 | def read_csv(fn): 698 | """ 699 | Read in a comma-separated value file as a DataFrame 700 | 701 | Parameters 702 | ---------- 703 | fn: string of file location 704 | 705 | Returns 706 | ------- 707 | A DataFrame 708 | """ 709 | pass 710 | -------------------------------------------------------------------------------- /tests/test_dataframe.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from numpy.testing import assert_array_equal 3 | import pytest 4 | 5 | import pandas_cub as pdc 6 | from tests import assert_df_equals 7 | 8 | pytestmark = pytest.mark.filterwarnings("ignore") 9 | 10 | a = np.array(['a', 'b', 'c']) 11 | b = np.array(['c', 'd', None]) 12 | c = np.random.rand(3) 13 | d = np.array([True, False, True]) 14 | e = np.array([1, 2, 3]) 15 | df = pdc.DataFrame({'a': a, 'b': b, 'c': c, 'd': d, 'e': e}) 16 | 17 | 18 | class TestDataFrameCreation: 19 | 20 | def test_input_types(self): 21 | with pytest.raises(TypeError): 22 | pdc.DataFrame([1, 2, 3]) 23 | 24 | with pytest.raises(TypeError): 25 | pdc.DataFrame({1: 5, 'b': 10}) 26 | 27 | with pytest.raises(TypeError): 28 | pdc.DataFrame({'a': np.array([1]), 'b': 10}) 29 | 30 | with pytest.raises(ValueError): 31 | pdc.DataFrame({'a': np.array([1]), 32 | 'b': np.array([[1]])}) 33 | 34 | # correct construction. no error 35 | pdc.DataFrame({'a': np.array([1]), 36 | 'b': np.array([1])}) 37 | 38 | def test_array_length(self): 39 | with pytest.raises(ValueError): 40 | pdc.DataFrame({'a': np.array([1, 2]), 41 | 'b': np.array([1])}) 42 | # correct construction. no error 43 | pdc.DataFrame({'a': np.array([1, 2]), 44 | 'b': np.array([5, 10])}) 45 | 46 | def test_unicode_to_object(self): 47 | a_object = a.astype('O') 48 | assert_array_equal(df._data['a'], a_object) 49 | assert_array_equal(df._data['b'], b) 50 | assert_array_equal(df._data['c'], c) 51 | assert_array_equal(df._data['d'], d) 52 | assert_array_equal(df._data['e'], e) 53 | 54 | def test_len(self): 55 | assert len(df) == 3 56 | 57 | def test_columns(self): 58 | assert df.columns == ['a', 'b', 'c', 'd', 'e'] 59 | 60 | def test_set_columns(self): 61 | with pytest.raises(TypeError): 62 | df.columns = 5 63 | 64 | with pytest.raises(ValueError): 65 | df.columns = ['a', 'b'] 66 | 67 | with pytest.raises(TypeError): 68 | df.columns = [1, 2, 3, 4, 5] 69 | 70 | with pytest.raises(ValueError): 71 | df.columns = ['f', 'f', 'g', 'h', 'i'] 72 | 73 | df.columns = ['f', 'g', 'h', 'i', 'j'] 74 | assert df.columns == ['f', 'g', 'h', 'i', 'j'] 75 | 76 | # set it back 77 | df.columns = ['a', 'b', 'c', 'd', 'e'] 78 | assert df.columns == ['a', 'b', 'c', 'd', 'e'] 79 | 80 | def test_shape(self): 81 | assert df.shape == (3, 5) 82 | 83 | def test_values(self): 84 | values = np.column_stack((a, b, c, d, e)) 85 | assert_array_equal(df.values, values) 86 | 87 | def test_dtypes(self): 88 | cols = np.array(['a', 'b', 'c', 'd', 'e'], dtype='O') 89 | dtypes = np.array(['string', 'string', 'float', 'bool', 'int'], dtype='O') 90 | 91 | df_result = df.dtypes 92 | df_answer = pdc.DataFrame({'Column Name': cols, 93 | 'Data Type': dtypes}) 94 | assert_df_equals(df_result, df_answer) 95 | 96 | 97 | class TestSelection: 98 | 99 | def test_one_column(self): 100 | assert_array_equal(df['a'].values[:, 0], a) 101 | assert_array_equal(df['c'].values[:, 0], c) 102 | 103 | def test_multiple_columns(self): 104 | cols = ['a', 'c'] 105 | df_result = df[cols] 106 | df_answer = pdc.DataFrame({'a': a, 'c': c}) 107 | assert_df_equals(df_result, df_answer) 108 | 109 | def test_simple_boolean(self): 110 | bool_arr = np.array([True, False, False]) 111 | df_bool = pdc.DataFrame({'col': bool_arr}) 112 | df_result = df[df_bool] 113 | df_answer = pdc.DataFrame({'a': a[bool_arr], 'b': b[bool_arr], 114 | 'c': c[bool_arr], 'd': d[bool_arr], 115 | 'e': e[bool_arr]}) 116 | assert_df_equals(df_result, df_answer) 117 | 118 | with pytest.raises(ValueError): 119 | df_bool = pdc.DataFrame({'col': bool_arr, 'col2': bool_arr}) 120 | df[df_bool] 121 | 122 | with pytest.raises(TypeError): 123 | df_bool = pdc.DataFrame({'col': np.array[1, 2, 3]}) 124 | 125 | def test_one_column_tuple(self): 126 | assert_df_equals(df[:, 'a'], pdc.DataFrame({'a': a})) 127 | 128 | def test_multiple_columns_tuple(self): 129 | cols = ['a', 'c'] 130 | df_result = df[:, cols] 131 | df_answer = pdc.DataFrame({'a': a, 'c': c}) 132 | assert_df_equals(df_result, df_answer) 133 | 134 | def test_int_selcetion(self): 135 | assert_df_equals(df[:, 3], pdc.DataFrame({'d': d})) 136 | 137 | def test_simultaneous_tuple(self): 138 | with pytest.raises(TypeError): 139 | s = set() 140 | df[s] 141 | 142 | with pytest.raises(ValueError): 143 | df[1, 2, 3] 144 | 145 | def test_single_element(self): 146 | df_answer = pdc.DataFrame({'e': np.array([2])}) 147 | assert_df_equals(df[1, 'e'], df_answer) 148 | 149 | def test_all_row_selections(self): 150 | df1 = pdc.DataFrame({'a': np.array([True, False, True]), 151 | 'b': np.array([1, 3, 5])}) 152 | with pytest.raises(ValueError): 153 | df[df1, 'e'] 154 | 155 | with pytest.raises(TypeError): 156 | df[df1['b'], 'c'] 157 | 158 | df_result = df[df1['a'], 'c'] 159 | df_answer = pdc.DataFrame({'c': c[[True, False, True]]}) 160 | assert_df_equals(df_result, df_answer) 161 | 162 | df_result = df[[1, 2], 0] 163 | df_answer = pdc.DataFrame({'a': a[[1, 2]]}) 164 | assert_df_equals(df_result, df_answer) 165 | 166 | df_result = df[1:, 0] 167 | assert_df_equals(df_result, df_answer) 168 | 169 | def test_list_columns(self): 170 | df_answer = pdc.DataFrame({'c': c, 'e': e}) 171 | assert_df_equals(df[:, [2, 4]], df_answer) 172 | assert_df_equals(df[:, [2, 'e']], df_answer) 173 | assert_df_equals(df[:, ['c', 'e']], df_answer) 174 | 175 | df_result = df[2, ['a', 'e']] 176 | df_answer = pdc.DataFrame({'a': a[[2]], 'e': e[[2]]}) 177 | assert_df_equals(df_result, df_answer) 178 | 179 | df_answer = pdc.DataFrame({'c': c[[1, 2]], 'e': e[[1, 2]]}) 180 | assert_df_equals(df[[1, 2], ['c', 'e']], df_answer) 181 | 182 | df1 = pdc.DataFrame({'a': np.array([True, False, True]), 183 | 'b': np.array([1, 3, 5])}) 184 | df_answer = pdc.DataFrame({'c': c[[0, 2]], 'e': e[[0, 2]]}) 185 | assert_df_equals(df[df1['a'], ['c', 'e']], df_answer) 186 | 187 | def test_col_slice(self): 188 | df_answer = pdc.DataFrame({'a': a, 'b': b, 'c': c}) 189 | assert_df_equals(df[:, :3], df_answer) 190 | 191 | df_answer = pdc.DataFrame({'a': a[::2], 'b': b[::2], 'c': c[::2]}) 192 | assert_df_equals(df[::2, :3], df_answer) 193 | 194 | df_answer = pdc.DataFrame({'a': a[::2], 'b': b[::2], 'c': c[::2], 'd': d[::2], 'e': e[::2]}) 195 | assert_df_equals(df[::2, :], df_answer) 196 | 197 | with pytest.raises(TypeError): 198 | df[:, set()] 199 | 200 | def test_tab_complete(self): 201 | assert ['a', 'b', 'c', 'd', 'e'] == df._ipython_key_completions_() 202 | 203 | def test_new_column(self): 204 | df_result = pdc.DataFrame({'a': a, 'b': b, 'c': c, 'd': d, 'e': e}) 205 | f = np.array([1.5, 23, 4.11]) 206 | df_result['f'] = f 207 | df_answer = pdc.DataFrame({'a': a, 'b': b, 'c': c, 'd': d, 'e': e, 'f': f}) 208 | assert_df_equals(df_result, df_answer) 209 | 210 | df_result = pdc.DataFrame({'a': a, 'b': b, 'c': c, 'd': d, 'e': e}) 211 | df_result['f'] = True 212 | f = np.repeat(True, 3) 213 | df_answer = pdc.DataFrame({'a': a, 'b': b, 'c': c, 'd': d, 'e': e, 'f': f}) 214 | assert_df_equals(df_result, df_answer) 215 | 216 | df_result = pdc.DataFrame({'a': a, 'b': b, 'c': c, 'd': d, 'e': e}) 217 | f = np.array([1.5, 23, 4.11]) 218 | df_result['c'] = f 219 | df_answer = pdc.DataFrame({'a': a, 'b': b, 'c': f, 'd': d, 'e': e}) 220 | assert_df_equals(df_result, df_answer) 221 | 222 | with pytest.raises(NotImplementedError): 223 | df[['a', 'b']] = 5 224 | 225 | with pytest.raises(ValueError): 226 | df['a'] = np.random.rand(5, 5) 227 | 228 | with pytest.raises(ValueError): 229 | df['a'] = np.random.rand(5) 230 | 231 | with pytest.raises(ValueError): 232 | df['a'] = df[['a', 'b']] 233 | 234 | with pytest.raises(ValueError): 235 | df1 = pdc.DataFrame({'a': np.random.rand(5)}) 236 | df['a'] = df1 237 | 238 | with pytest.raises(TypeError): 239 | df['a'] = set() 240 | 241 | def test_head_tail(self): 242 | df_result = df.head(2) 243 | df_answer = pdc.DataFrame({'a': a[:2], 'b': b[:2], 'c': c[:2], 244 | 'd': d[:2], 'e': e[:2]}) 245 | assert_df_equals(df_result, df_answer) 246 | 247 | df_result = df.tail(2) 248 | df_answer = pdc.DataFrame({'a': a[-2:], 'b': b[-2:], 'c': c[-2:], 249 | 'd':d[-2:], 'e': e[-2:]}) 250 | assert_df_equals(df_result, df_answer) 251 | 252 | 253 | a1 = np.array(['a', 'b', 'c']) 254 | b1 = np.array([11, 5, 8]) 255 | c1 = np.array([3.4, np.nan, 5.1]) 256 | df1 = pdc.DataFrame({'a': a1, 'b': b1, 'c': c1}) 257 | 258 | a2 = np.array([True, False]) 259 | b2 = np.array([True, True]) 260 | c2 = np.array([False, True]) 261 | df2 = pdc.DataFrame({'a': a2, 'b': b2, 'c': c2}) 262 | 263 | 264 | class TestAggregation: 265 | 266 | 267 | def test_min(self): 268 | df_result = df1.min() 269 | df_answer = pdc.DataFrame({'a': np.array(['a'], dtype='O'), 270 | 'b': np.array([5]), 271 | 'c': np.array([np.nan])}) 272 | assert_df_equals(df_result, df_answer) 273 | 274 | def test_max(self): 275 | df_result = df1.max() 276 | df_answer = pdc.DataFrame({'a': np.array(['c'], dtype='O'), 277 | 'b': np.array([11]), 278 | 'c': np.array([np.nan])}) 279 | assert_df_equals(df_result, df_answer) 280 | 281 | def test_mean(self): 282 | df_result = df1.mean() 283 | df_answer = pdc.DataFrame({'b': np.array([8.]), 284 | 'c': np.array([np.nan])}) 285 | assert_df_equals(df_result, df_answer) 286 | 287 | def test_median(self): 288 | df_result = df1.median() 289 | df_answer = pdc.DataFrame({'b': np.array([8]), 290 | 'c': np.array([np.nan])}) 291 | assert_df_equals(df_result, df_answer) 292 | 293 | def test_sum(self): 294 | df_result = df1.sum() 295 | df_answer = pdc.DataFrame({'a': np.array(['abc'], dtype='O'), 296 | 'b': np.array([24]), 297 | 'c': np.array([np.nan])}) 298 | assert_df_equals(df_result, df_answer) 299 | 300 | def test_var(self): 301 | df_result = df1.var() 302 | df_answer = pdc.DataFrame({'b': np.array([b1.var()]), 303 | 'c': np.array([np.nan])}) 304 | assert_df_equals(df_result, df_answer) 305 | 306 | def test_std(self): 307 | df_result = df1.std() 308 | df_answer = pdc.DataFrame({'b': np.array([b1.std()]), 309 | 'c': np.array([np.nan])}) 310 | assert_df_equals(df_result, df_answer) 311 | 312 | def test_all(self): 313 | df_result = df2.all() 314 | df_answer = pdc.DataFrame({'a': np.array([False]), 315 | 'b': np.array([True]), 316 | 'c': np.array([False])}) 317 | assert_df_equals(df_result, df_answer) 318 | 319 | def test_any(self): 320 | df_result = df2.any() 321 | df_answer = pdc.DataFrame({'a': np.array([True]), 322 | 'b': np.array([True]), 323 | 'c': np.array([True])}) 324 | assert_df_equals(df_result, df_answer) 325 | 326 | def test_argmax(self): 327 | df_result = df1.argmax() 328 | df_answer = pdc.DataFrame({'a': np.array([2]), 329 | 'b': np.array([0]), 330 | 'c': np.array([1])}) 331 | assert_df_equals(df_result, df_answer) 332 | 333 | def test_argmin(self): 334 | df_result = df1.argmin() 335 | df_answer = pdc.DataFrame({'a': np.array([0]), 336 | 'b': np.array([1]), 337 | 'c': np.array([1])}) 338 | assert_df_equals(df_result, df_answer) 339 | 340 | 341 | a3 = np.array(['a', None, 'c']) 342 | b3 = np.array([11, 5, 8]) 343 | c3 = np.array([3.4, np.nan, 5.1]) 344 | df3 = pdc.DataFrame({'a': a3, 'b': b3, 'c': c3}) 345 | 346 | a4 = np.array(['a', 'a', 'c'], dtype='O') 347 | b4 = np.array([11, 5, 5]) 348 | c4 = np.array([3.4, np.nan, 3.4]) 349 | df4 = pdc.DataFrame({'a': a4, 'b': b4, 'c': c4}) 350 | 351 | 352 | class TestOtherMethods: 353 | 354 | def test_isna(self): 355 | df_result = df3.isna() 356 | df_answer = pdc.DataFrame({'a': np.array([False, True, False]), 357 | 'b': np.array([False, False, False]), 358 | 'c': np.array([False, True, False])}) 359 | assert_df_equals(df_result, df_answer) 360 | 361 | def test_count(self): 362 | df_result = df3.count() 363 | df_answer = pdc.DataFrame({'a': np.array([2]), 364 | 'b': np.array([3]), 365 | 'c': np.array([2])}) 366 | assert_df_equals(df_result, df_answer) 367 | 368 | def test_unique(self): 369 | df_result = df4.unique() 370 | assert_array_equal(df_result[0].values[:, 0], np.unique(a4)) 371 | assert_array_equal(df_result[1].values[:, 0], np.unique(b4)) 372 | assert_array_equal(df_result[2].values[:, 0], np.unique(c4)) 373 | 374 | def test_nunique(self): 375 | df_result = df4.nunique() 376 | df_answer = pdc.DataFrame({'a': np.array([2]), 377 | 'b': np.array([2]), 378 | 'c': np.array([2])}) 379 | assert_df_equals(df_result, df_answer) 380 | 381 | def test_rename(self): 382 | df_result = df4.rename({'a': 'A', 'c': 'C'}) 383 | df_answer = pdc.DataFrame({'A': a4, 'b': b4, 'C': c4}) 384 | assert_df_equals(df_result, df_answer) 385 | 386 | def test_drop(self): 387 | df_result = df4.drop(['a', 'b']) 388 | df_answer = pdc.DataFrame({'c': c4}) 389 | assert_df_equals(df_result, df_answer) 390 | 391 | 392 | a42 = np.array([-11, 5, 3]) 393 | b42 = np.array([3.4, 5.1, -6]) 394 | df42 = pdc.DataFrame({'a': a42, 'b': b42}) 395 | 396 | 397 | class TestNonAgg: 398 | 399 | def test_abs(self): 400 | df_result = df42.abs() 401 | df_answer = pdc.DataFrame({'a': np.abs(a42), 'b': np.abs(b42)}) 402 | assert_df_equals(df_result, df_answer) 403 | 404 | def test_cummin(self): 405 | df_result = df42.cummin() 406 | df_answer = pdc.DataFrame({'a': np.array([-11, -11, -11]), 407 | 'b': np.array([3.4, 3.4, -6])}) 408 | assert_df_equals(df_result, df_answer) 409 | 410 | def test_cummax(self): 411 | df_result = df42.cummax() 412 | df_answer = pdc.DataFrame({'a': np.array([-11, 5, 5]), 413 | 'b': np.array([3.4, 5.1, 5.1])}) 414 | assert_df_equals(df_result, df_answer) 415 | 416 | def test_cumsum(self): 417 | df_result = df42.cumsum() 418 | df_answer = pdc.DataFrame({'a': np.array([-11, -6, -3]), 419 | 'b': np.array([3.4, 8.5, 2.5])}) 420 | assert_df_equals(df_result, df_answer) 421 | 422 | def test_clip(self): 423 | df_result = df42.clip(0, 4) 424 | df_answer = pdc.DataFrame({'a': np.array([0, 4, 3]), 425 | 'b': np.array([3.4, 4, 0])}) 426 | assert_df_equals(df_result, df_answer) 427 | 428 | def test_round(self): 429 | df_result = df42.round(0) 430 | df_answer = pdc.DataFrame({'a': np.array([-11, 5, 3]), 431 | 'b': np.array([3, 5, -6])}) 432 | assert_df_equals(df_result, df_answer) 433 | 434 | def test_copy(self): 435 | assert_df_equals(df42, df42.copy()) 436 | 437 | def test_diff(self): 438 | df_result = df42.diff(1) 439 | df_answer = pdc.DataFrame({'a': np.array([np.nan, 16, -2]), 440 | 'b': np.array([np.nan, 1.7, -11.1])}) 441 | assert_df_equals(df_result, df_answer) 442 | 443 | def test_pct_change(self): 444 | df_result = df42.pct_change(1) 445 | df_answer = pdc.DataFrame({'a': np.array([np.nan, 16 / -11, -2 / 5]), 446 | 'b': np.array([np.nan, 1.7 / 3.4, -11.1 / 5.1])}) 447 | assert_df_equals(df_result, df_answer) 448 | 449 | 450 | a5 = np.array([11, 5]) 451 | b5 = np.array([3.4, 5.1]) 452 | df5 = pdc.DataFrame({'a': a5, 'b': b5}) 453 | 454 | 455 | class TestOperators: 456 | 457 | def test_add(self): 458 | df_result = df5 + 3 459 | df_answer = pdc.DataFrame({'a': a5 + 3, 'b': b5 + 3}) 460 | assert_df_equals(df_result, df_answer) 461 | 462 | df_result = 3 + df5 463 | assert_df_equals(df_result, df_answer) 464 | 465 | def test_sub(self): 466 | df_result = df5 - 3 467 | df_answer = pdc.DataFrame({'a': a5 - 3, 'b': b5 - 3}) 468 | assert_df_equals(df_result, df_answer) 469 | 470 | df_result = 3 - df5 471 | df_answer = pdc.DataFrame({'a': 3 - a5, 'b': 3 - b5}) 472 | assert_df_equals(df_result, df_answer) 473 | 474 | def test_mul(self): 475 | df_result = df5 * 3 476 | df_answer = pdc.DataFrame({'a': a5 * 3, 'b': b5 * 3}) 477 | assert_df_equals(df_result, df_answer) 478 | 479 | df_result = 3 * df5 480 | assert_df_equals(df_result, df_answer) 481 | 482 | def test_truediv(self): 483 | df_result = df5 / 3 484 | df_answer = pdc.DataFrame({'a': a5 / 3, 'b': b5 / 3}) 485 | assert_df_equals(df_result, df_answer) 486 | 487 | df_result = 3 / df5 488 | df_answer = pdc.DataFrame({'a': 3 / a5, 'b': 3 / b5}) 489 | assert_df_equals(df_result, df_answer) 490 | 491 | def test_floordiv(self): 492 | df_result = df5 // 3 493 | df_answer = pdc.DataFrame({'a': a5 // 3, 'b': b5 // 3}) 494 | assert_df_equals(df_result, df_answer) 495 | 496 | df_result = 3 // df5 497 | df_answer = pdc.DataFrame({'a': 3 // a5, 'b': 3 // b5}) 498 | assert_df_equals(df_result, df_answer) 499 | 500 | def test_pow(self): 501 | df_result = df5 ** 3 502 | df_answer = pdc.DataFrame({'a': a5 ** 3, 'b': b5 ** 3}) 503 | assert_df_equals(df_result, df_answer) 504 | 505 | df_result = 2 ** df5 506 | df_answer = pdc.DataFrame({'a': 2 ** a5, 'b': 2 ** b5}) 507 | assert_df_equals(df_result, df_answer) 508 | 509 | def test_gt_lt(self): 510 | df_result = df5 > 3 511 | df_answer = pdc.DataFrame({'a': a5 > 3, 'b': b5 > 3}) 512 | assert_df_equals(df_result, df_answer) 513 | 514 | df_result = df5 < 2 515 | df_answer = pdc.DataFrame({'a': a5 < 2, 'b': b5 < 2}) 516 | assert_df_equals(df_result, df_answer) 517 | 518 | def test_ge_le(self): 519 | df_result = df5 >= 3 520 | df_answer = pdc.DataFrame({'a': a5 >= 3, 'b': b5 >= 3}) 521 | assert_df_equals(df_result, df_answer) 522 | 523 | df_result = df5 < 2 524 | df_answer = pdc.DataFrame({'a': a5 <= 2, 'b': b5 <= 2}) 525 | assert_df_equals(df_result, df_answer) 526 | 527 | def test_eq_ne(self): 528 | df_result = df5 == 3 529 | df_answer = pdc.DataFrame({'a': a5 == 3, 'b': b5 == 3}) 530 | assert_df_equals(df_result, df_answer) 531 | 532 | df_result = df5 != 2 533 | df_answer = pdc.DataFrame({'a': a5 != 2, 'b': b5 != 2}) 534 | assert_df_equals(df_result, df_answer) 535 | 536 | 537 | a6 = np.array(['b', 'c', 'a', 'a', 'b']) 538 | b6 = np.array([3.4, 5.1, 2, 1, 6]) 539 | df6 = pdc.DataFrame({'a': a6, 'b': b6}) 540 | 541 | a7 = np.array(['b', 'a', 'a', 'a', 'b']) 542 | b7 = np.array([3.4, 5.1, 2, 1, 6]) 543 | df7 = pdc.DataFrame({'a': a7, 'b': b7}) 544 | 545 | 546 | class TestMoreMethods: 547 | 548 | def test_sort_values(self): 549 | df_result = df6.sort_values('a') 550 | a = np.array(['a', 'a', 'b', 'b', 'c']) 551 | b = np.array([2, 1, 3.4, 6, 5.1]) 552 | df_answer = pdc.DataFrame({'a': a, 'b': b}) 553 | assert_df_equals(df_result, df_answer) 554 | 555 | def test_sort_values_desc(self): 556 | df_result = df6.sort_values('a', asc=False) 557 | a = np.array(['c', 'b', 'b', 'a', 'a']) 558 | b = np.array([5.1, 6, 3.4, 1,2]) 559 | df_answer = pdc.DataFrame({'a': a, 'b': b}) 560 | assert_df_equals(df_result, df_answer) 561 | 562 | def test_sort_values_two(self): 563 | df_result = df7.sort_values(['a', 'b']) 564 | a = np.array(['a', 'a', 'a', 'b', 'b']) 565 | b = np.array([1, 2, 5.1, 3.4, 6]) 566 | df_answer = pdc.DataFrame({'a': a, 'b': b}) 567 | assert_df_equals(df_result, df_answer) 568 | 569 | def test_sort_values_two_desc(self): 570 | df_result = df7.sort_values(['a', 'b'], asc=False) 571 | a = np.array(['a', 'a', 'a', 'b', 'b']) 572 | b = np.array([1, 2, 5.1, 3.4, 6]) 573 | df_answer = pdc.DataFrame({'a': a[::-1], 'b': b[::-1]}) 574 | assert_df_equals(df_result, df_answer) 575 | 576 | def test_sample(self): 577 | df_result = df7.sample(2, seed=1) 578 | df_answer = pdc.DataFrame({'a': np.array(['a', 'a'], dtype=object), 579 | 'b': np.array([2., 5.1])}) 580 | assert_df_equals(df_result, df_answer) 581 | 582 | df_result = df7.sample(frac=.7, seed=1) 583 | df_answer = pdc.DataFrame({'a': np.array(['a', 'a', 'b'], dtype=object), 584 | 'b': np.array([2., 5.1, 6.])}) 585 | assert_df_equals(df_result, df_answer) 586 | 587 | with pytest.raises(TypeError): 588 | df7.sample(2.5) 589 | 590 | with pytest.raises(ValueError): 591 | df7.sample(frac=-2) 592 | 593 | 594 | a8 = np.array(['b', 'a', 'a', 'a', 'b', 'a', 'a', 'b']) 595 | b8 = np.array(['B', 'A', 'A', 'A', 'B', 'B', 'B', 'A']) 596 | c8 = np.array([1, 2, 3, 4, 5, 6, 7, 8]) 597 | df8 = pdc.DataFrame({'a': a8, 'b': b8, 'c': c8}) 598 | 599 | 600 | class TestGrouping: 601 | 602 | def test_value_counts(self): 603 | df_temp = pdc.DataFrame({'state': np.array(['texas', 'texas', 'texas', 'florida', 'florida', 'florida', 'florida', 'ohio']), 604 | 'fruit': np.array(['a', 'a', 'a', 'a', 'b', 'b', 'b', 'a'])}) 605 | df_results = df_temp.value_counts() 606 | df_answer = pdc.DataFrame({'state': np.array(['florida', 'texas', 'ohio'], dtype=object), 607 | 'count': np.array([4, 3, 1])}) 608 | assert_df_equals(df_results[0], df_answer) 609 | 610 | df_answer = pdc.DataFrame({'fruit': np.array(['a', 'b'], dtype=object), 611 | 'count': np.array([5, 3])}) 612 | assert_df_equals(df_results[1], df_answer) 613 | 614 | def test_value_counts_normalize(self): 615 | df_temp = pdc.DataFrame({'state': np.array(['texas', 'texas', 'texas', 'florida', 'florida', 'florida', 'florida', 'ohio']), 616 | 'fruit': np.array(['a', 'a', 'a', 'a', 'b', 'b', 'b', 'a'])}) 617 | df_results = df_temp.value_counts(normalize=True) 618 | df_answer = pdc.DataFrame({'state': np.array(['florida', 'texas', 'ohio'], dtype=object), 619 | 'count': np.array([.5, .375, .125])}) 620 | assert_df_equals(df_results[0], df_answer) 621 | 622 | df_answer = pdc.DataFrame({'fruit': np.array(['a', 'b'], dtype=object), 623 | 'count': np.array([.625, .375])}) 624 | assert_df_equals(df_results[1], df_answer) 625 | 626 | def test_pivot_table_rows_or_cols(self): 627 | df_result = df8.pivot_table(rows='a') 628 | df_answer = pdc.DataFrame({'a': np.array(['a', 'b'], dtype=object), 629 | 'size': np.array([5, 3])}) 630 | assert_df_equals(df_result, df_answer) 631 | 632 | df_result = df8.pivot_table(rows='a', values='c', aggfunc='sum') 633 | df_answer = pdc.DataFrame({'a': np.array(['a', 'b'], dtype=object), 634 | 'sum': np.array([22, 14])}) 635 | assert_df_equals(df_result, df_answer) 636 | 637 | df_result = df8.pivot_table(columns='b') 638 | df_answer = pdc.DataFrame({'A': np.array([4]), 639 | 'B': np.array([4])}) 640 | assert_df_equals(df_result, df_answer) 641 | 642 | df_result = df8.pivot_table(columns='a', values='c', aggfunc='sum') 643 | df_answer = pdc.DataFrame({'a': np.array([22]), 'b': np.array([14])}) 644 | assert_df_equals(df_result, df_answer) 645 | 646 | def test_pivot_table_both(self): 647 | df_result = df8.pivot_table(rows='a', columns='b', values='c', aggfunc='sum') 648 | df_answer = pdc.DataFrame({'a': np.array(['a', 'b'], dtype=object), 649 | 'A': np.array([9., 8.]), 650 | 'B': np.array([13., 6.])}) 651 | assert_df_equals(df_result, df_answer) 652 | 653 | 654 | movie = np.array(['field of dreams', 'star wars'], dtype='O') 655 | num = np.array(['5.1', '6'], dtype='O') 656 | df_string = pdc.DataFrame({'movie': movie, 'num': num}) 657 | 658 | 659 | class TestStrings: 660 | 661 | def test_capitalize(self): 662 | result = df_string.str.capitalize('movie') 663 | movie = np.array(['Field of dreams', 'Star wars'], dtype='O') 664 | answer = pdc.DataFrame({'movie': movie}) 665 | assert_df_equals(result, answer) 666 | 667 | def test_center(self): 668 | result = df_string.str.center('movie', 20, '-') 669 | movie = np.array(['--field of dreams---', '-----star wars------'], dtype='O') 670 | answer = pdc.DataFrame({'movie': movie}) 671 | assert_df_equals(result, answer) 672 | 673 | def test_count(self): 674 | result = df_string.str.count('movie', 'e') 675 | movie = np.array([2, 0]) 676 | answer = pdc.DataFrame({'movie': movie}) 677 | assert_df_equals(result, answer) 678 | 679 | def test_startswith(self): 680 | result = df_string.str.startswith('movie', 'field') 681 | movie = np.array([True, False]) 682 | answer = pdc.DataFrame({'movie': movie}) 683 | assert_df_equals(result, answer) 684 | 685 | def test_endswith(self): 686 | result = df_string.str.endswith('movie', 's') 687 | movie = np.array([True, True]) 688 | answer = pdc.DataFrame({'movie': movie}) 689 | assert_df_equals(result, answer) 690 | 691 | def test_find(self): 692 | result = df_string.str.find('movie', 'ar') 693 | movie = np.array([-1, 2]) 694 | answer = pdc.DataFrame({'movie': movie}) 695 | assert_df_equals(result, answer) 696 | 697 | def test_len(self): 698 | result = df_string.str.len('movie') 699 | movie = np.array([15, 9]) 700 | answer = pdc.DataFrame({'movie': movie}) 701 | assert_df_equals(result, answer) 702 | 703 | def test_get(self): 704 | result = df_string.str.get('movie', 5) 705 | movie = np.array([' ', 'w'], dtype='O') 706 | answer = pdc.DataFrame({'movie': movie}) 707 | assert_df_equals(result, answer) 708 | 709 | def test_index(self): 710 | with pytest.raises(ValueError): 711 | df_string.str.index('movie', 'z') 712 | 713 | def test_isalnum(self): 714 | result = df_string.str.isalnum('num') 715 | num = np.array([False, True]) 716 | answer = pdc.DataFrame({'num': num}) 717 | assert_df_equals(result, answer) 718 | 719 | def test_isalpha(self): 720 | result = df_string.str.isalpha('num') 721 | num = np.array([False, False]) 722 | answer = pdc.DataFrame({'num': num}) 723 | assert_df_equals(result, answer) 724 | 725 | def test_isdecimal(self): 726 | result = df_string.str.isdecimal('num') 727 | num = np.array([False, True]) 728 | answer = pdc.DataFrame({'num': num}) 729 | assert_df_equals(result, answer) 730 | 731 | def test_isnumeric(self): 732 | result = df_string.str.isnumeric('num') 733 | num = np.array([False, True]) 734 | answer = pdc.DataFrame({'num': num}) 735 | assert_df_equals(result, answer) 736 | 737 | def test_islower(self): 738 | result = df_string.str.islower('movie') 739 | movie = np.array([True, True]) 740 | answer = pdc.DataFrame({'movie': movie}) 741 | assert_df_equals(result, answer) 742 | 743 | def test_isupper(self): 744 | result = df_string.str.isupper('movie') 745 | movie = np.array([False, False]) 746 | answer = pdc.DataFrame({'movie': movie}) 747 | assert_df_equals(result, answer) 748 | 749 | def test_isspace(self): 750 | result = df_string.str.isspace('num') 751 | num = np.array([False, False]) 752 | answer = pdc.DataFrame({'num': num}) 753 | assert_df_equals(result, answer) 754 | 755 | def test_istitle(self): 756 | result = df_string.str.istitle('num') 757 | num = np.array([False, False]) 758 | answer = pdc.DataFrame({'num': num}) 759 | assert_df_equals(result, answer) 760 | 761 | def test_lstrip(self): 762 | result = df_string.str.lstrip('movie', 'fies') 763 | movie = np.array(['ld of dreams', 'tar wars'], dtype='O') 764 | answer = pdc.DataFrame({'movie': movie}) 765 | assert_df_equals(result, answer) 766 | 767 | def test_rstrip(self): 768 | result = df_string.str.rstrip('movie', 's') 769 | movie = np.array(['field of dream', 'star war'], dtype='O') 770 | answer = pdc.DataFrame({'movie': movie}) 771 | assert_df_equals(result, answer) 772 | 773 | def test_strip(self): 774 | result = df_string.str.strip('movie', 'fs') 775 | movie = np.array(['ield of dream', 'tar war'], dtype='O') 776 | answer = pdc.DataFrame({'movie': movie}) 777 | assert_df_equals(result, answer) 778 | 779 | def test_replace(self): 780 | result = df_string.str.replace('movie', 's', 'Z') 781 | movie = np.array(['field of dreamZ', 'Ztar warZ'], dtype='O') 782 | answer = pdc.DataFrame({'movie': movie}) 783 | assert_df_equals(result, answer) 784 | 785 | def test_swapcase(self): 786 | result = df_string.str.swapcase('movie') 787 | movie = np.array(['FIELD OF DREAMS', 'STAR WARS'], dtype='O') 788 | answer = pdc.DataFrame({'movie': movie}) 789 | assert_df_equals(result, answer) 790 | 791 | def test_title(self): 792 | result = df_string.str.title('movie') 793 | movie = np.array(['Field Of Dreams', 'Star Wars'], dtype='O') 794 | answer = pdc.DataFrame({'movie': movie}) 795 | assert_df_equals(result, answer) 796 | 797 | def test_upper(self): 798 | result = df_string.str.upper('movie') 799 | movie = np.array(['FIELD OF DREAMS', 'STAR WARS'], dtype='O') 800 | answer = pdc.DataFrame({'movie': movie}) 801 | assert_df_equals(result, answer) 802 | 803 | def test_zfill(self): 804 | result = df_string.str.zfill('movie', 16) 805 | movie = np.array(['0field of dreams', '0000000star wars'], dtype='O') 806 | answer = pdc.DataFrame({'movie': movie}) 807 | assert_df_equals(result, answer) 808 | 809 | 810 | df_emp = pdc.read_csv('data/employee.csv') 811 | 812 | 813 | class TestReadCSV: 814 | 815 | def test_columns(self): 816 | result = df_emp.columns 817 | answer = ['dept', 'race', 'gender', 'salary'] 818 | assert result == answer 819 | 820 | def test_data_types(self): 821 | df_result = df_emp.dtypes 822 | cols = np.array(['dept', 'race', 'gender', 'salary'], dtype='O') 823 | dtypes = np.array(['string', 'string', 'string', 'int'], dtype='O') 824 | df_answer = pdc.DataFrame({'Column Name': cols, 825 | 'Data Type': dtypes}) 826 | assert_df_equals(df_result, df_answer) 827 | 828 | def test_sum(self): 829 | result = df_emp['salary'].sum() 830 | answer = 86387875 831 | assert result == answer 832 | 833 | def test_head(self): 834 | data = {'dept': np.array(['Houston Police Department-HPD', 835 | 'Houston Fire Department (HFD)', 836 | 'Houston Police Department-HPD', 837 | 'Public Works & Engineering-PWE', 838 | 'Houston Airport System (HAS)'], dtype='O'), 839 | 'race': np.array(['White', 'White', 'Black', 'Asian', 'White'], dtype='O'), 840 | 'gender': np.array(['Male', 'Male', 'Male', 'Male', 'Male'], dtype='O'), 841 | 'salary': np.array([45279, 63166, 66614, 71680, 42390])} 842 | result = df_emp.head() 843 | answer = pdc.DataFrame(data) 844 | assert_df_equals(result, answer) -------------------------------------------------------------------------------- /pandas_cub_final/__init__.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | __version__ = '0.0.1' 4 | 5 | 6 | class DataFrame: 7 | 8 | def __init__(self, data): 9 | """ 10 | A DataFrame holds two-dimensional heterogeneous data. Create it by 11 | passing a dictionary of NumPy arrays to the values parameter 12 | 13 | Parameters 14 | ---------- 15 | data: dict 16 | A dictionary of strings mapped to NumPy arrays. The key will 17 | become the column name. 18 | """ 19 | # check for correct input types 20 | self._check_input_types(data) 21 | 22 | # check for equal array lengths 23 | self._check_array_lengths(data) 24 | 25 | # convert unicode arrays to object 26 | self._data = self._convert_unicode_to_object(data) 27 | 28 | # Allow for special methods for strings 29 | self.str = StringMethods(self) 30 | self._add_docs() 31 | 32 | def _check_input_types(self, data): 33 | if not isinstance(data, dict): 34 | raise TypeError("`data` must be a dictionary of 1-D NumPy arrays") 35 | 36 | for col_name, values in data.items(): 37 | if not isinstance(col_name, str): 38 | raise TypeError('All column names must be a string') 39 | if not isinstance(values, np.ndarray): 40 | raise TypeError('All values must be a 1-D NumPy array') 41 | else: 42 | if values.ndim != 1: 43 | raise ValueError('Each value must be a 1-D NumPy array') 44 | 45 | def _check_array_lengths(self, data): 46 | for i, values in enumerate(data.values()): 47 | if i == 0: 48 | length = len(values) 49 | if length != len(values): 50 | raise ValueError('All values must be the same length') 51 | 52 | def _convert_unicode_to_object(self, data): 53 | new_data = {} 54 | for col_name, values in data.items(): 55 | if values.dtype.kind == 'U': 56 | new_data[col_name] = values.astype('O') 57 | else: 58 | new_data[col_name] = values 59 | return new_data 60 | 61 | def __len__(self): 62 | """ 63 | Make the builtin len function work with our dataframe 64 | 65 | Returns 66 | ------- 67 | int: the number of rows in the dataframe 68 | """ 69 | return len(next(iter(self._data.values()))) 70 | 71 | @property 72 | def columns(self): 73 | """ 74 | _data holds column names mapped to arrays 75 | take advantage of internal ordering of dictionaries to 76 | put columns in correct order in list. Only works in 3.6+ 77 | 78 | Returns 79 | ------- 80 | list of column names 81 | """ 82 | return list(self._data) 83 | 84 | @columns.setter 85 | def columns(self, columns): 86 | """ 87 | Must supply a list of columns as strings the same length 88 | as the current DataFrame 89 | 90 | Parameters 91 | ---------- 92 | columns: list of strings 93 | 94 | Returns 95 | ------- 96 | Nones 97 | """ 98 | if not isinstance(columns, list): 99 | raise TypeError('New columns must be a list') 100 | if len(columns) != len(self.columns): 101 | raise ValueError(f'New column length must be {len(self._data)}') 102 | else: 103 | for col in columns: 104 | if not isinstance(col, str): 105 | raise TypeError('New column names must be strings') 106 | if len(columns) != len(set(columns)): 107 | raise ValueError('Column names must be unique') 108 | 109 | new_data = dict(zip(columns, self._data.values())) 110 | self._data = new_data 111 | 112 | @property 113 | def shape(self): 114 | """ 115 | Returns 116 | ------- 117 | two-item tuple of number of rows and columns 118 | """ 119 | return len(self), len(self.columns) 120 | 121 | def _repr_html_(self): 122 | """ 123 | Used to create a string of HTML to nicely display the DataFrame 124 | in a Jupyter Notebook. Different string formatting is used for 125 | different data types. 126 | 127 | The structure of the HTML is as follows: 128 | 129 | 130 | 131 | 132 | ... 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | ... 141 | 142 | 143 | ... 144 | 145 | 146 | 147 | ... 148 | 149 | 150 | 151 |
datadata
{i}datadata
{i}datadata
152 | """ 153 | html = '' 154 | for col in self.columns: 155 | html += f"" 156 | 157 | html += '' 158 | html += "" 159 | 160 | only_head = False 161 | num_head = 10 162 | num_tail = 10 163 | if len(self) <= 20: 164 | only_head = True 165 | num_head = len(self) 166 | 167 | for i in range(num_head): 168 | html += f'' 169 | for col, values in self._data.items(): 170 | kind = values.dtype.kind 171 | if kind == 'f': 172 | html += f'' 173 | elif kind == 'b': 174 | html += f'' 175 | elif kind == 'O': 176 | v = values[i] 177 | if v is None: 178 | v = 'None' 179 | html += f'' 180 | else: 181 | html += f'' 182 | html += '' 183 | 184 | if not only_head: 185 | html += '' 186 | for i in range(len(self.columns)): 187 | html += '' 188 | html += '' 189 | for i in range(-num_tail, 0): 190 | html += f'' 191 | for col, values in self._data.items(): 192 | kind = values.dtype.kind 193 | if kind == 'f': 194 | html += f'' 195 | elif kind == 'b': 196 | html += f'' 197 | elif kind == 'O': 198 | v = values[i] 199 | if v is None: 200 | v = 'None' 201 | html += f'' 202 | else: 203 | html += f'' 204 | html += '' 205 | 206 | html += '
{col:10}
{i}{values[i]:10.3f}{values[i]}{v:10}{values[i]:10}
......
{len(self) + i}{values[i]:10.3f}{values[i]}{v:10}{values[i]:10}
' 207 | return html 208 | 209 | @property 210 | def values(self): 211 | """ 212 | Returns 213 | ------- 214 | A single 2D NumPy array of the underlying data 215 | """ 216 | return np.column_stack(self._data.values()) 217 | 218 | @property 219 | def dtypes(self): 220 | """ 221 | Returns 222 | ------- 223 | A two-column DataFrame of column names in one column and 224 | their data type in the other 225 | """ 226 | DTYPE_NAME = {'O': 'string', 'i': 'int', 'f': 'float', 'b': 'bool'} 227 | col_arr = np.array(self.columns) 228 | dtypes = [] 229 | for values in self._data.values(): 230 | kind = values.dtype.kind 231 | dtype = DTYPE_NAME[kind] 232 | dtypes.append(dtype) 233 | 234 | return DataFrame({'Column Name': col_arr, 'Data Type': np.array(dtypes)}) 235 | 236 | def __getitem__(self, item): 237 | """ 238 | Use the brackets operator to simultaneously select rows and columns 239 | 240 | A single string selects one column -> df['colname'] 241 | A list of strings selects multiple columns -> df[['colname1', 'colname2']] 242 | A one column DataFrame of booleans that filters rows -> df[df_bool] 243 | 244 | Row and column selection simultaneously -> df[rs, cs] 245 | where cs and rs can be integers, slices, or a list of integers 246 | rs can also be a one-column boolean DataFrame 247 | 248 | Returns 249 | ------- 250 | A subset of the original DataFrame 251 | """ 252 | # select a single column -> df['colname'] 253 | if isinstance(item, str): 254 | return DataFrame({item: self._data[item]}) 255 | 256 | # select multiple columns -> df[['colname1', 'colname2']] 257 | if isinstance(item, list): 258 | return DataFrame({col: self._data[col] for col in item}) 259 | 260 | # boolean selection 261 | if isinstance(item, DataFrame): 262 | if item.shape[1] != 1: 263 | raise ValueError('Can only pass a one column DataFrame for selection') 264 | 265 | bool_arr = next(iter(item._data.values())) 266 | if bool_arr.dtype.kind != 'b': 267 | raise TypeError('DataFrame must be a boolean') 268 | 269 | new_data = {} 270 | for col, values in self._data.items(): 271 | new_data[col] = values[bool_arr] 272 | return DataFrame(new_data) 273 | 274 | if isinstance(item, tuple): 275 | return self._getitem_tuple(item) 276 | else: 277 | raise TypeError('Select with either a string, a list, or a row and column ' 278 | 'simultaneous selection') 279 | 280 | def _getitem_tuple(self, item): 281 | # simultaneous selection of rows and cols -> df[rs, cs] 282 | if len(item) != 2: 283 | raise ValueError('Pass either a single string or a two-item tuple inside the ' 284 | 'selection operator.') 285 | row_selection = item[0] 286 | col_selection = item[1] 287 | if isinstance(row_selection, int): 288 | row_selection = [row_selection] 289 | elif isinstance(row_selection, DataFrame): 290 | if row_selection.shape[1] != 1: 291 | raise ValueError('Can only pass a one column DataFrame for selection') 292 | row_selection = next(iter(row_selection._data.values())) 293 | if row_selection.dtype.kind != 'b': 294 | raise TypeError('DataFrame must be a boolean') 295 | elif not isinstance(row_selection, (list, slice)): 296 | raise TypeError('Row selection must be either an int, slice, list, or DataFrame') 297 | 298 | if isinstance(col_selection, int): 299 | col_selection = [self.columns[col_selection]] 300 | elif isinstance(col_selection, str): 301 | col_selection = [col_selection] 302 | elif isinstance(col_selection, list): 303 | new_col_selction = [] 304 | for col in col_selection: 305 | if isinstance(col, int): 306 | new_col_selction.append(self.columns[col]) 307 | else: 308 | new_col_selction.append(col) 309 | col_selection = new_col_selction 310 | elif isinstance(col_selection, slice): 311 | start = col_selection.start 312 | stop = col_selection.stop 313 | step = col_selection.step 314 | if isinstance(start, str): 315 | start = self.columns.index(col_selection.start) 316 | if isinstance(stop, str): 317 | stop = self.columns.index(col_selection.stop) + 1 318 | 319 | col_selection = self.columns[start:stop:step] 320 | else: 321 | raise TypeError('Column selection must be either an int, string, list, or slice') 322 | 323 | new_data = {} 324 | for col in col_selection: 325 | new_data[col] = self._data[col][row_selection] 326 | return DataFrame(new_data) 327 | 328 | def _ipython_key_completions_(self): 329 | # allows for tab completion when doing df['c 330 | return self.columns 331 | 332 | def __setitem__(self, key, value): 333 | # adds a new column or a overwrites an old column 334 | if not isinstance(key, str): 335 | raise NotImplementedError('Only able to set a single column') 336 | 337 | if isinstance(value, np.ndarray): 338 | if value.ndim != 1: 339 | raise ValueError('Setting array must be 1D') 340 | if len(value) != len(self): 341 | raise ValueError('Setting array must be same length as DataFrame') 342 | elif isinstance(value, DataFrame): 343 | if value.shape[1] != 1: 344 | raise ValueError('Setting DataFrame must be one column') 345 | if len(value) != len(self): 346 | raise ValueError('Setting and Calling DataFrames must be the same length') 347 | value = next(iter(value._data.values())) 348 | elif isinstance(value, (int, str, float, bool)): 349 | value = np.repeat(value, len(self)) 350 | else: 351 | raise TypeError('Setting value must either be a numpy array, ' 352 | 'DataFrame, integer, string, float, or boolean') 353 | 354 | if value.dtype.kind == 'U': 355 | value = value.astype('O') 356 | 357 | self._data[key] = value 358 | 359 | def head(self, n=5): 360 | """ 361 | Return the first n rows 362 | 363 | Parameters 364 | ---------- 365 | n: int 366 | 367 | Returns 368 | ------- 369 | DataFrame 370 | """ 371 | return self[:n, :] 372 | 373 | def tail(self, n=5): 374 | """ 375 | Return the last n rows 376 | 377 | Parameters 378 | ---------- 379 | n: int 380 | 381 | Returns 382 | ------- 383 | DataFrame 384 | """ 385 | return self[-n:, :] 386 | 387 | #### Aggregation Methods #### 388 | 389 | def min(self): 390 | return self._agg(np.min) 391 | 392 | def max(self): 393 | return self._agg(np.max) 394 | 395 | def mean(self): 396 | return self._agg(np.mean) 397 | 398 | def median(self): 399 | return self._agg(np.median) 400 | 401 | def sum(self): 402 | return self._agg(np.sum) 403 | 404 | def var(self): 405 | return self._agg(np.var) 406 | 407 | def std(self): 408 | return self._agg(np.std) 409 | 410 | def all(self): 411 | return self._agg(np.all) 412 | 413 | def any(self): 414 | return self._agg(np.any) 415 | 416 | def argmax(self): 417 | return self._agg(np.argmax) 418 | 419 | def argmin(self): 420 | return self._agg(np.argmin) 421 | 422 | def _agg(self, aggfunc): 423 | """ 424 | Generic aggregation function that applies the 425 | aggregation to each column 426 | 427 | Parameters 428 | ---------- 429 | aggfunc: str of the aggregation function name in NumPy 430 | 431 | Returns 432 | ------- 433 | A DataFrame 434 | """ 435 | new_data = {} 436 | for col, values in self._data.items(): 437 | try: 438 | val = aggfunc(values) 439 | except TypeError: 440 | continue 441 | new_data[col] = np.array([val]) 442 | return DataFrame(new_data) 443 | 444 | def isna(self): 445 | """ 446 | Determines whether each value in the DataFrame is missing or not 447 | 448 | Returns 449 | ------- 450 | A DataFrame of booleans the same size as the calling DataFrame 451 | """ 452 | new_data = {} 453 | for col, values in self._data.items(): 454 | kind = values.dtype.kind 455 | if kind == 'O': 456 | new_data[col] = values == None 457 | else: 458 | new_data[col] = np.isnan(values) 459 | return DataFrame(new_data) 460 | 461 | def count(self): 462 | """ 463 | Counts the number of non-missing values per column 464 | 465 | Returns 466 | ------- 467 | A DataFrame 468 | """ 469 | new_data = {} 470 | df = self.isna() 471 | length = len(self) 472 | for col, values in df._data.items(): 473 | val = length - values.sum() 474 | new_data[col] = np.array([val]) 475 | return DataFrame(new_data) 476 | 477 | def unique(self): 478 | """ 479 | Finds the unique values of each column 480 | 481 | Returns 482 | ------- 483 | A list of one-column DataFrames 484 | """ 485 | dfs = [] 486 | for col, values in self._data.items(): 487 | uniques = np.unique(values) 488 | dfs.append(DataFrame({col: uniques})) 489 | if len(dfs) == 1: 490 | return dfs[0] 491 | return dfs 492 | 493 | def nunique(self): 494 | """ 495 | Find the number of unique values in each column 496 | 497 | Returns 498 | ------- 499 | A DataFrame 500 | """ 501 | new_data = {} 502 | for col, value in self._data.items(): 503 | new_data[col] = np.array([len(np.unique(value))]) 504 | return DataFrame(new_data) 505 | 506 | def value_counts(self, normalize=False): 507 | """ 508 | Returns the frequency of each unique value for each column 509 | 510 | Parameters 511 | ---------- 512 | normalize: bool 513 | If True, returns the relative frequencies (percent) 514 | 515 | Returns 516 | ------- 517 | A list of DataFrames or a single DataFrame if one column 518 | """ 519 | dfs = [] 520 | for col, values in self._data.items(): 521 | keys, raw_counts = np.unique(values, return_counts=True) 522 | 523 | order = np.argsort(-raw_counts) 524 | keys = keys[order] 525 | raw_counts = raw_counts[order] 526 | 527 | if normalize: 528 | raw_counts = raw_counts / raw_counts.sum() 529 | df = DataFrame({col: keys, 'count': raw_counts}) 530 | dfs.append(df) 531 | if len(dfs) == 1: 532 | return dfs[0] 533 | return dfs 534 | 535 | def rename(self, columns): 536 | """ 537 | Renames columns in the DataFrame 538 | 539 | Parameters 540 | ---------- 541 | columns: dict 542 | A dictionary mapping the old column name to the new column name 543 | 544 | Returns 545 | ------- 546 | A DataFrame 547 | """ 548 | if not isinstance(columns, dict): 549 | raise TypeError('`columns` must be a dictionary') 550 | 551 | new_data = {} 552 | for col, values in self._data.items(): 553 | new_data[columns.get(col, col)] = values 554 | return DataFrame(new_data) 555 | 556 | def drop(self, columns): 557 | """ 558 | Drops one or more columns from a DataFrame 559 | 560 | Parameters 561 | ---------- 562 | columns: str or list of strings 563 | 564 | Returns 565 | ------- 566 | A DataFrame 567 | """ 568 | if isinstance(columns, str): 569 | columns = [columns] 570 | elif not isinstance(columns, list): 571 | raise TypeError('`columns` must be either a string or a list') 572 | new_data = {} 573 | for col, values in self._data.items(): 574 | if col not in columns: 575 | new_data[col] = values 576 | return DataFrame(new_data) 577 | 578 | #### Non-Aggregation Methods #### 579 | 580 | def abs(self): 581 | """ 582 | Takes the absolute value of each value in the DataFrame 583 | 584 | Returns 585 | ------- 586 | A DataFrame 587 | """ 588 | return self._non_agg(np.abs) 589 | 590 | def cummin(self): 591 | """ 592 | Finds cumulative minimum by column 593 | 594 | Returns 595 | ------- 596 | A DataFrame 597 | """ 598 | return self._non_agg(np.minimum.accumulate) 599 | 600 | def cummax(self): 601 | """ 602 | Finds cumulative maximum by column 603 | 604 | Returns 605 | ------- 606 | A DataFrame 607 | """ 608 | return self._non_agg(np.maximum.accumulate) 609 | 610 | def cumsum(self): 611 | """ 612 | Finds cumulative sum by column 613 | 614 | Returns 615 | ------- 616 | A DataFrame 617 | """ 618 | return self._non_agg(np.cumsum) 619 | 620 | def clip(self, lower=None, upper=None): 621 | """ 622 | All values less than lower will be set to lower 623 | All values greater than upper will be set to upper 624 | 625 | Parameters 626 | ---------- 627 | lower: number or None 628 | upper: number or None 629 | 630 | Returns 631 | ------- 632 | A DataFrame 633 | """ 634 | return self._non_agg(np.clip, a_min=lower, a_max=upper) 635 | 636 | def round(self, n): 637 | """ 638 | Rounds values to the nearest n decimals 639 | 640 | Returns 641 | ------- 642 | A DataFrame 643 | """ 644 | return self._non_agg(np.round, 'if', decimals=n) 645 | 646 | def copy(self): 647 | """ 648 | Copies the DataFrame 649 | 650 | Returns 651 | ------- 652 | A DataFrame 653 | """ 654 | return self._non_agg(np.copy) 655 | 656 | def _non_agg(self, funcname, kinds='bif', **kwargs): 657 | """ 658 | Generic non-aggregation function 659 | 660 | Parameters 661 | ---------- 662 | funcname: numpy function 663 | args: extra arguments for certain functions 664 | 665 | Returns 666 | ------- 667 | A DataFrame 668 | """ 669 | new_data = {} 670 | for col, values in self._data.items(): 671 | if values.dtype.kind in kinds: 672 | values = funcname(values, **kwargs) 673 | else: 674 | values = values.copy() 675 | new_data[col] = values 676 | return DataFrame(new_data) 677 | 678 | def diff(self, n=1): 679 | """ 680 | Take the difference between the current value and 681 | the nth value above it. 682 | 683 | Parameters 684 | ---------- 685 | n: int 686 | 687 | Returns 688 | ------- 689 | A DataFrame 690 | """ 691 | def func(values): 692 | values = values.astype('float') 693 | values_shifted = np.roll(values, n) 694 | values = values - values_shifted 695 | if n >= 0: 696 | values[:n] = np.NAN 697 | else: 698 | values[n:] = np.NAN 699 | return values 700 | return self._non_agg(func) 701 | 702 | def pct_change(self, n=1): 703 | """ 704 | Take the percentage difference between the current value and 705 | the nth value above it. 706 | 707 | Parameters 708 | ---------- 709 | n: int 710 | 711 | Returns 712 | ------- 713 | A DataFrame 714 | """ 715 | def func(values): 716 | values = values.astype('float') 717 | values_shifted = np.roll(values, n) 718 | values = values - values_shifted 719 | if n >= 0: 720 | values[:n] = np.NAN 721 | else: 722 | values[n:] = np.NAN 723 | return values / values_shifted 724 | return self._non_agg(func) 725 | 726 | #### Arithmetic and Comparison Operators #### 727 | 728 | def __add__(self, other): 729 | return self._oper('__add__', other) 730 | 731 | def __radd__(self, other): 732 | return self._oper('__radd__', other) 733 | 734 | def __sub__(self, other): 735 | return self._oper('__sub__', other) 736 | 737 | def __rsub__(self, other): 738 | return self._oper('__rsub__', other) 739 | 740 | def __mul__(self, other): 741 | return self._oper('__mul__', other) 742 | 743 | def __rmul__(self, other): 744 | return self._oper('__rmul__', other) 745 | 746 | def __truediv__(self, other): 747 | return self._oper('__truediv__', other) 748 | 749 | def __rtruediv__(self, other): 750 | return self._oper('__rtruediv__', other) 751 | 752 | def __floordiv__(self, other): 753 | return self._oper('__floordiv__', other) 754 | 755 | def __rfloordiv__(self, other): 756 | return self._oper('__rfloordiv__', other) 757 | 758 | def __pow__(self, other): 759 | return self._oper('__pow__', other) 760 | 761 | def __rpow__(self, other): 762 | return self._oper('__rpow__', other) 763 | 764 | def __gt__(self, other): 765 | return self._oper('__gt__', other) 766 | 767 | def __lt__(self, other): 768 | return self._oper('__lt__', other) 769 | 770 | def __ge__(self, other): 771 | return self._oper('__ge__', other) 772 | 773 | def __le__(self, other): 774 | return self._oper('__le__', other) 775 | 776 | def __ne__(self, other): 777 | return self._oper('__ne__', other) 778 | 779 | def __eq__(self, other): 780 | return self._oper('__eq__', other) 781 | 782 | def _oper(self, op, other): 783 | """ 784 | Generic operator method 785 | 786 | Parameters 787 | ---------- 788 | op: str name of special method 789 | other: the other object being operated on 790 | 791 | Returns 792 | ------- 793 | A DataFrame 794 | """ 795 | if isinstance(other, DataFrame): 796 | if other.shape[1] != 1: 797 | raise ValueError('`other` must be a one-column DataFrame') 798 | other = next(iter(other._data.values())) 799 | new_data = {} 800 | for col, values in self._data.items(): 801 | func = getattr(values, op) 802 | new_data[col] = func(other) 803 | return DataFrame(new_data) 804 | 805 | def sort_values(self, by, asc=True): 806 | """ 807 | Sort the DataFrame by one or more values 808 | 809 | Parameters 810 | ---------- 811 | by: str or list of column names 812 | asc: boolean of sorting order 813 | 814 | Returns 815 | ------- 816 | A DataFrame 817 | """ 818 | if isinstance(by, str): 819 | order = np.argsort(self._data[by]) 820 | elif isinstance(by, list): 821 | cols = [self._data[col] for col in by[::-1]] 822 | order = np.lexsort(cols) 823 | else: 824 | raise TypeError('`by` must be a str or a list') 825 | 826 | if not asc: 827 | order = order[::-1] 828 | return self[order.tolist(), :] 829 | 830 | def sample(self, n=None, frac=None, replace=False, seed=None): 831 | """ 832 | Randomly samples rows the DataFrame 833 | 834 | Parameters 835 | ---------- 836 | n: int 837 | number of rows to return 838 | frac: float 839 | Proportion of the data to sample 840 | replace: bool 841 | Whether or not to sample with replacement 842 | seed: int 843 | Seeds the random number generator 844 | 845 | Returns 846 | ------- 847 | A DataFrame 848 | """ 849 | if seed: 850 | np.random.seed(seed) 851 | if frac is not None: 852 | if frac <= 0: 853 | raise ValueError('`frac` must be positive') 854 | n = int(frac * len(self)) 855 | if n is not None: 856 | if not isinstance(n, int): 857 | raise TypeError('`n` must be an int') 858 | rows = np.random.choice(np.arange(len(self)), size=n, replace=replace).tolist() 859 | return self[rows, :] 860 | 861 | def pivot_table(self, rows=None, columns=None, values=None, aggfunc=None): 862 | """ 863 | Creates a pivot table from one or two 'grouping' columns. 864 | 865 | Parameters 866 | ---------- 867 | rows: str of column name to group by 868 | Optional 869 | columns: str of column name to group by 870 | Optional 871 | values: str of column name to aggregate 872 | Required 873 | aggfunc: str of aggregation function 874 | 875 | Returns 876 | ------- 877 | A DataFrame 878 | """ 879 | if rows is None and columns is None: 880 | raise ValueError('`rows` or `columns` cannot both be `None`') 881 | 882 | if values is not None: 883 | val_data = self._data[values] 884 | if aggfunc is None: 885 | raise ValueError('You must provide `aggfunc` when `values` is provided.') 886 | else: 887 | if aggfunc is None: 888 | aggfunc = 'size' 889 | val_data = np.empty(len(self)) 890 | else: 891 | raise ValueError('You cannot provide `aggfunc` when `values` is None') 892 | 893 | if rows is not None: 894 | row_data = self._data[rows] 895 | 896 | if columns is not None: 897 | col_data = self._data[columns] 898 | 899 | if rows is None: 900 | pivot_type = 'columns' 901 | elif columns is None: 902 | pivot_type = 'rows' 903 | else: 904 | pivot_type = 'all' 905 | 906 | from collections import defaultdict 907 | d = defaultdict(list) 908 | if pivot_type == 'columns': 909 | for group, val in zip(col_data, val_data): 910 | d[group].append(val) 911 | elif pivot_type == 'rows': 912 | for group, val in zip(row_data, val_data): 913 | d[group].append(val) 914 | else: 915 | for group1, group2, val in zip(row_data, col_data, val_data): 916 | d[(group1, group2)].append(val) 917 | 918 | agg_dict = {} 919 | for group, vals in d.items(): 920 | arr = np.array(vals) 921 | func = getattr(np, aggfunc) 922 | agg_dict[group] = func(arr) 923 | 924 | new_data = {} 925 | if pivot_type == 'columns': 926 | for col_name in sorted(agg_dict): 927 | value = agg_dict[col_name] 928 | new_data[col_name] = np.array([value]) 929 | elif pivot_type == 'rows': 930 | row_array = np.array(list(agg_dict.keys())) 931 | val_array = np.array(list(agg_dict.values())) 932 | 933 | order = np.argsort(row_array) 934 | new_data[rows] = row_array[order] 935 | new_data[aggfunc] = val_array[order] 936 | else: 937 | row_set = set() 938 | col_set = set() 939 | for group in agg_dict: 940 | row_set.add(group[0]) 941 | col_set.add(group[1]) 942 | row_list = sorted(row_set) 943 | col_list = sorted(col_set) 944 | new_data = {} 945 | new_data[rows] = np.array(row_list) 946 | for col in col_list: 947 | new_vals = [] 948 | for row in row_list: 949 | new_val = agg_dict.get((row, col), np.nan) 950 | new_vals.append(new_val) 951 | new_data[col] = np.array(new_vals) 952 | return DataFrame(new_data) 953 | 954 | def _add_docs(self): 955 | agg_names = ['min', 'max', 'mean', 'median', 'sum', 'var', 956 | 'std', 'any', 'all', 'argmax', 'argmin'] 957 | agg_doc = \ 958 | """ 959 | Find the {} of each column 960 | 961 | Returns 962 | ------- 963 | DataFrame 964 | """ 965 | for name in agg_names: 966 | getattr(DataFrame, name).__doc__ = agg_doc.format(name) 967 | 968 | 969 | class StringMethods: 970 | 971 | def __init__(self, df): 972 | self._df = df 973 | 974 | def capitalize(self, col): 975 | return self._str_method(str.capitalize, col) 976 | 977 | def center(self, col, width, fillchar=None): 978 | if fillchar is None: 979 | fillchar = ' ' 980 | return self._str_method(str.center, col, width, fillchar) 981 | 982 | def count(self, col, sub, start=None, stop=None): 983 | return self._str_method(str.count, col, sub, start, stop) 984 | 985 | def endswith(self, col, suffix, start=None, stop=None): 986 | return self._str_method(str.endswith, col, suffix, start, stop) 987 | 988 | def startswith(self, col, suffix, start=None, stop=None): 989 | return self._str_method(str.startswith, col, suffix, start, stop) 990 | 991 | def find(self, col, sub, start=None, stop=None): 992 | return self._str_method(str.find, col, sub, start, stop) 993 | 994 | def len(self, col): 995 | return self._str_method(str.__len__, col) 996 | 997 | def get(self, col, item): 998 | return self._str_method(str.__getitem__, col, item) 999 | 1000 | def index(self, col, sub, start=None, stop=None): 1001 | return self._str_method(str.index, col, sub, start, stop) 1002 | 1003 | def isalnum(self, col): 1004 | return self._str_method(str.isalnum, col) 1005 | 1006 | def isalpha(self, col): 1007 | return self._str_method(str.isalpha, col) 1008 | 1009 | def isdecimal(self, col): 1010 | return self._str_method(str.isdecimal, col) 1011 | 1012 | def islower(self, col): 1013 | return self._str_method(str.islower, col) 1014 | 1015 | def isnumeric(self, col): 1016 | return self._str_method(str.isnumeric, col) 1017 | 1018 | def isspace(self, col): 1019 | return self._str_method(str.isspace, col) 1020 | 1021 | def istitle(self, col): 1022 | return self._str_method(str.istitle, col) 1023 | 1024 | def isupper(self, col): 1025 | return self._str_method(str.isupper, col) 1026 | 1027 | def lstrip(self, col, chars): 1028 | return self._str_method(str.lstrip, col, chars) 1029 | 1030 | def rstrip(self, col, chars): 1031 | return self._str_method(str.rstrip, col, chars) 1032 | 1033 | def strip(self, col, chars): 1034 | return self._str_method(str.strip, col, chars) 1035 | 1036 | def replace(self, col, old, new, count=None): 1037 | if count is None: 1038 | count = -1 1039 | return self._str_method(str.replace, col, old, new, count) 1040 | 1041 | def swapcase(self, col): 1042 | return self._str_method(str.swapcase, col) 1043 | 1044 | def title(self, col): 1045 | return self._str_method(str.title, col) 1046 | 1047 | def lower(self, col): 1048 | return self._str_method(str.lower, col) 1049 | 1050 | def upper(self, col): 1051 | return self._str_method(str.upper, col) 1052 | 1053 | def zfill(self, col, width): 1054 | return self._str_method(str.zfill, col, width) 1055 | 1056 | def encode(self, col, encoding='utf-8', errors='strict'): 1057 | return self._str_method(str.encode, col, encoding, errors) 1058 | 1059 | def _str_method(self, method, col, *args): 1060 | old_values = self._df._data[col] 1061 | if old_values.dtype.kind != 'O': 1062 | raise TypeError('The `str` accessor only works with string columns') 1063 | new_values = [] 1064 | for val in old_values: 1065 | if val is None: 1066 | new_values.append(val) 1067 | else: 1068 | new_val = method(val, *args) 1069 | new_values.append(new_val) 1070 | arr = np.array(new_values) 1071 | return DataFrame({col: arr}) 1072 | 1073 | 1074 | def read_csv(fn): 1075 | """ 1076 | Read in a comma-separated value file as a DataFrame 1077 | 1078 | Parameters 1079 | ---------- 1080 | fn: string of file location 1081 | 1082 | Returns 1083 | ------- 1084 | A DataFrame 1085 | """ 1086 | from collections import defaultdict 1087 | values = defaultdict(list) 1088 | with open(fn) as f: 1089 | header = f.readline() 1090 | column_names = header.strip('\n').split(',') 1091 | for line in f: 1092 | vals = line.strip('\n').split(',') 1093 | for val, name in zip(vals, column_names): 1094 | values[name].append(val) 1095 | new_data = {} 1096 | for col, vals in values.items(): 1097 | try: 1098 | new_data[col] = np.array(vals, dtype='int') 1099 | except ValueError: 1100 | try: 1101 | new_data[col] = np.array(vals, dtype='float') 1102 | except ValueError: 1103 | new_data[col] = np.array(vals, dtype='O') 1104 | return DataFrame(new_data) 1105 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Build a Data Analysis Library from Scratch 2 | 3 | This repository contains a detailed project that teaches you how to build your own Python data analysis library, pandas_cub, from scratch. The end result will be a fully-functioning library similar to pandas. 4 | 5 | ## Video Course 6 | 7 | A detailed video course is available [on Dunder Data][22] that walks you through the entire project. 8 | 9 | ## Target Student 10 | 11 | This project is targeted towards those who understand the fundamentals 12 | of Python and would like to immerse themselves into a larger, highly structured project that covers some advanced topics. It also touches upon a few crucial areas of software development. 13 | 14 | ## Pre-Requisites 15 | 16 | This is not a project suitable for beginning Python users. At a minimum you will need to have a solid understanding of the fundamentals such as: 17 | 18 | * Basic types and common data structures (lists, tuples, sets, and dictionaries) 19 | * Control flow with if/else statements and for loops (especially when iterating through lists or dictionaries) 20 | * Raising and handling exceptions 21 | * You will need to have covered the basics of classes and object-oriented programming. If you have never defined a class before, I strongly recommend going through an introductory tutorial on them first. [This one][15] from Corey Shafer is good. 22 | 23 | In addition to those Python basics, the main workhorse is the numpy library. The project will be difficult to complete without prior exposure to numpy. This [quickstart guide][16] might be beneficial for those needing to catch up quickly. 24 | 25 | We will not be using the pandas library within our code, but will be implementing many of the same method names with similar parameters and functionality. It will be very beneficial to have some exposure to pandas before beginning. 26 | 27 | ## Objectives 28 | 29 | Most data scientists who use Python rely on pandas. In this assignment, we will build pandas cub, a library that implements many of the most common and useful methods found in pandas. Pandas Cub will: 30 | 31 | * Have a DataFrame class with data stored in numpy arrays 32 | * Select subsets of data with the brackets operator 33 | * Use special methods defined in the Python data model 34 | * Have a nicely formatted display of the DataFrame in the notebook 35 | * Implement aggregation methods - sum, min, max, mean, median, etc... 36 | * Implement non-aggregation methods such as isna, unique, rename, drop 37 | * Group by one or two columns 38 | * Have methods specific to string columns 39 | * Read in data from a comma-separated value file 40 | 41 | In addition to these items specific to data analysis, you will also learn about: 42 | 43 | * Creating a development environment 44 | * Test-Driven Development 45 | 46 | ## Setting up the Development Environment 47 | 48 | I recommend creating a new environment using the conda package manager. If you do not have conda, you can [download it here][0] along with the entire Anaconda distribution. Choose Python 3. When beginning development on a new library, it's a good idea to use a completely separate environment to write your code. 49 | 50 | ### Create the environment with the `environment.yml` file 51 | 52 | Conda allows you to automate the environment creation with an `environment.yml` file. The contents of the file are minimal and are displayed below. 53 | 54 | ```yml 55 | name: pandas_cub 56 | dependencies: 57 | - python=3.6 58 | - pandas 59 | - jupyter 60 | - pytest 61 | ``` 62 | 63 | This file will be used to create a new environment named `pandas_cub`. It will install Python 3.6 in a completely separate directory in your file system along with pandas, jupyter, and pytest. There will actually be many more packages installed as those libraries have dependencies of their own. Visit [this page][2] for more information on conda environments. 64 | 65 | ### Command to create new environment 66 | 67 | In the top level directory of this repository, where the `environment.yml` file is located, run the following from your command line. 68 | 69 | `conda env create -f environment.yml` 70 | 71 | The above command will take some time to complete. Once it completes, the environment will be created. 72 | 73 | ### List the environments 74 | 75 | Run the command `conda env list` to show all the environments you have. There will be a `*` next to the active environment, which will likely be `base`, the default environment that everyone starts in. 76 | 77 | ### Activate the pandas_cub environment 78 | 79 | Creating the environment does not mean it is active. You must activate in order to use it. Use the following command to activate it. 80 | 81 | `conda activate pandas_cub` 82 | 83 | You should see `pandas_cub` in parentheses preceding your command prompt. You can run the command `conda env list` to confirm that the `*` has moved to `pandas_cub`. 84 | 85 | ### Deactivate environment 86 | 87 | You should only use the `pandas_cub` environment to develop this library. When you are done with this session, run the command `conda deactivate` to return to your default conda environment. 88 | 89 | ## Test-Driven Development with pytest 90 | 91 | The completion of each part of this project is predicated upon passing the 92 | tests written in the `test_dataframe.py` module inside the `tests` folder. 93 | 94 | We will rely upon the [pytest library][1] to test our code. We installed it along with a command line tool with the same name during our environment creation. 95 | 96 | [Test-Driven development][3] is a popular approach for developing software. It involves writing tests first and then writing code that passes the tests. 97 | 98 | ### Testing 99 | 100 | All the tests have already been written and are located in the `test_dataframe.py` module found in the `tests` directory. There are about 100 tests that you will need to pass to complete the project. To run all the tests in this file run the following on the command line. 101 | 102 | `$ pytest tests/test_dataframe.py` 103 | 104 | If you run this command right now, all the tests will fail. You should see a line of red capital 'F's. As you complete the steps in the project, you will start passing the tests. There are about 100 total tests. Once all the tests are passed, the project will be complete. 105 | 106 | ### Automated test discovery 107 | 108 | The pytest library has [rules for automated test discovery][4]. It isn't necessary to supply the path to the test module if your directories and module names follow those rules. You can simply run `pytest` to run all the tests in this library. 109 | 110 | ### Running specific tests 111 | 112 | If you open up the `test_dataframe.py` file, you will see the tests grouped under different classes. Each method of the classes represents exactly one test. To run all the tests within a single class, append two colons followed by the class name. The following is a concrete example: 113 | 114 | `$ pytest tests/test_dataframe.py::TestDataFrameCreation` 115 | 116 | It is possible to run just a single test by appending two more colons followed by the method name. Another concrete example follows: 117 | 118 | `$ pytest tests/test_dataframe.py::TestDataFrameCreation::test_input_types` 119 | 120 | ## Installing an IPython Kernel for Jupyter 121 | 122 | Although we have set up our development environment to work on the command line, we need to make a few more steps to hook it up with Jupyter Notebooks correctly. 123 | 124 | This is important, because Jupyter Notebooks are good for manually testing code as you will see below. 125 | 126 | ### Launch a Jupyter Notebook 127 | 128 | Within the `pandas_cub` environment, launch a Jupyter Notebook with the command `jupyter notebook`. When the home page finishes loading in your browser open up the `Test Notebook.ipynb` notebook. 129 | 130 | ### Changing the environment within Jupyter 131 | 132 | Although we launched our Jupyter Notebook within the `pandas_cub` environment, our code may not be executed within the `pandas_cub` environment at first. If that sounds bizarre and non-intuitive then you have reached the same conclusion as me. It is possible to run any python executable from a Jupyter Notebook regardless of the environment that it was launched from. 133 | 134 | If you run the first cell of the notebook (shown below) you can verify the location in your file system where Python is getting executed. 135 | 136 | **One of two possibilities can happen** 137 | 138 | If the value outputted from `sys.executable` is in the pandas_cub environment, it will have a path that ends like this: 139 | 140 | `anaconda3/envs/pandas_cub/bin/python` 141 | 142 | If this is your output, you can skip the rest of this step. 143 | 144 | If the value outputted from `sys.executable` ends with the following: 145 | 146 | `anaconda3/bin/python` 147 | 148 | then you are actually not executing python from the `pandas_cub` environment. You need to complete the rest of the step. 149 | 150 | ![][17] 151 | 152 | Exit out of Jupyter and return to the command line. We need to create a new [Kernel][18], a program that "runs and introspects the user’s code" 153 | 154 | Thankfully there is a command we can run with the `ipykernel` package to automatically create a new kernel. The `ipykernel` package should get installed during environment creation. 155 | 156 | The following command creates the Kernel. Make sure you have activated the `pandas_cub` environment first. You can read more about this command [in the documentation][19]. 157 | 158 | ```bash 159 | python -m ipykernel install --user --name pandas_cub --display-name "Python (pandas_cub)" 160 | ``` 161 | 162 | You may verify that the `pandas_cub` Kernel was created with the following command: 163 | 164 | ```bash 165 | jupyter kernelspec list 166 | ``` 167 | 168 | ### Launch Jupyter Again 169 | 170 | Launch Jupyter again and open up the `Test Notebook.ipynb` notebook. You will still NOT be in the `pandas_cub` environment. You need to navigate inside the 'Kernel' menu above and into 'Change kernel'. Finally, you can select the 'Python (pandas_cub)' Kernel which will place you in the right environment. The kernel will restart once you choose this option. 171 | 172 | ![][20] 173 | 174 | Run the first cell of the notebook again and you should see that the Python executable is coming from the `pandas_cub` environment directory. 175 | 176 | ![][21] 177 | 178 | You don't have to do this procedure again or this notebook. From now on, it will open up using the `pandas_cub` Kernel that was created. You can of course change the Kernel again but this is its new default. To verify this, shutdown the notebook and restart it. 179 | 180 | If you start a new notebook, you will have the option to decide which Kernel you would like to run it with. 181 | 182 | ## Inspecting the `__init__.py` File 183 | 184 | You will be editing a single file for this project - the `__init__.py` file 185 | found in the `pandas_cub` directory. It contains skeleton code for the entire project. You won't be defining your own classes or methods, but you will be filling out the method bodies. 186 | 187 | Open up this file now. You will see many incomplete methods that have the keyword `pass` as their last line. These are the methods that you will be editing. A few methods are complete and won't need editing. 188 | 189 | ### Docstrings 190 | 191 | You'll notice that all the methods have triple quoted strings directly beneath them. These strings are the documentation or 'docstrings'. All docstrings begin with a short summary of what the method does. A Parameters section follows thats lists each parameter, its type, and a description of how its used. The docstrings end with a Returns section that informs the user of what type of object is returned. It's important to read them as they contain information on how to complete the methods. 192 | 193 | There are many ways you can write docstrings, but these follow the [numpy docstring guide][7]. There are many other sections you may add to them as well. 194 | 195 | ## Importing pandas_cub 196 | 197 | Return back to the Jupyter notebook, which should already be open. This notebook is at the same level as the inner `pandas_cub` directory. This means that we can import `pandas_cub` directly into our namespace without changing directories. Technically, `pandas_cub` is a Python **package**, which is a directory containing a `__init__.py` file. It is this initialization file that gets run when we write `import pandas_cub as pdc`. 198 | 199 | ## Manually Test in a Jupyter Notebook 200 | 201 | During development, it's good to have a place to manually experiment with your new code so you can see it in action. We will be using the Jupyter Notebook to quickly see how our DataFrame is changing. 202 | 203 | ### Autoreloading 204 | 205 | The second cell loads a notebook magic extension which automatically reloads code from files that have changed. Normally, we would have to restart the kernel if we made changes to our code to see it reflect its current state. This magic command saves us from doing this. 206 | 207 | ### Imports 208 | 209 | Along with `pandas_cub` `pandas_cub_final` is also imported so you can see how the completed object is supposed to behave. 210 | 211 | We import the `pandas` library so that you can compare and contrast its functionality. 212 | 213 | ### A test DataFrame 214 | 215 | A simple test DataFrame is created for `pandas_cub`, `pandas_cub_final`, and `pandas`. The output for all three DataFrames are produced in the notebook. There currently is no nice visual representation for `pandas_cub` DataFrames. 216 | 217 | ## Starting Pandas Cub 218 | 219 | Keep the `__init__.py` file open at all times. This is the only file that you will be editing. Read and complete each numbered step below. Edit the method indicated in each step and then run the test. Once you pass that test, move on to the next step. 220 | 221 | ### The answer is in pandas_cub_final 222 | 223 | The `pandas_cub_final` directory contains the completed `__init__.py` file with the code that passes all the tests. Only look at this file after you have attempted to complete each step on your own. 224 | 225 | ### 1. Check DataFrame constructor input types 226 | 227 | Our DataFrame class is constructed with a single parameter, `data`. Python will call the special `__init__` method when first constructing our DataFrame. This method has already been completed for you and you will not need to edit it. Within the `__init__` method, several more methods are called that check to see if the user has passed it valid data. You will be editing these methods during the next few steps. 228 | 229 | In this step, you will only be editing the `_check_input_types` method. This is the first method called within the `__init__` method. It will ensure that our users have passed us a valid `data` parameter. 230 | 231 | We are going to force our users to set `data` as a dictionary that has strings as the keys and one-dimensional numpy arrays as the values. The keys will eventually become the column names and the arrays will be the values of those columns. 232 | 233 | Specifically, `_check_input_types` must do the following: 234 | 235 | * raise a `TypeError` if `data` is not a dictionary 236 | * raise a `TypeError` if the keys of `data` are not strings 237 | * raise a `TypeError` if the values of `data` are not numpy arrays 238 | * raise a `ValueError` if the values of `data` are not 1-dimensional 239 | 240 | Edit this method now. Use the `isinstance` function to help you determine the type of an object. 241 | 242 | Run the following command to test this step. Once you have passed this test move on to the next step. 243 | 244 | `$ pytest tests/test_dataframe.py::TestDataFrameCreation::test_input_types` 245 | 246 | ### 2. Check array lengths 247 | 248 | We are now guaranteed that `data` is a dictionary of strings mapped to one-dimensional arrays. Each column of data in our DataFrame must have the same number of elements. In this step, you must ensure that this is the case. Edit the `_check_array_lengths` method and raise a `ValueError` if any of the arrays differ in length. 249 | 250 | Run the following test: 251 | 252 | `$ pytest tests/test_dataframe.py::TestDataFrameCreation::test_array_length` 253 | 254 | ### 3. Convert unicode arrays to object 255 | 256 | Whenever you create a numpy array of Python strings, it will default the data type of that array to unicode. Take a look at the following simple numpy array created from strings. Its data type, found in the `dtype` attribute is shown to be 'U' plus the length of the longest string. 257 | 258 | ```python 259 | >>> a = np.array(['cat', 'dog', 'snake']) 260 | >>> a.dtype 261 | dtype('>> a.dtype.kind 270 | 'U' 271 | ``` 272 | 273 | Pass the `astype` array method the correct kind character to change its type. 274 | 275 | Edit the `_convert_unicode_to_object` method and fill the dictionary `new_data` with the converted arrays. The result of this method will be returned and assigned as the `_data` instance variable. 276 | 277 | Run `test_unicode_to_object` to test. 278 | 279 | ### A note on names that begin with a single underscore 280 | 281 | So far we have seen a few examples of attribute and method names within our DataFrame class that begin with a single underscore. These names are intended to be 'private' and not directly accessed by our users. This doesn't prevent our users from accessing these names as nothing is technically private in Python, but it is common convention and discussed in [this section of the PEP8 style guide][9]. 282 | 283 | Most IDEs will not show these private methods as choices to the users which is a good thing. They are not at all meant to be accessed by them. 284 | 285 | ### 4. Find the number of rows in the DataFrame with the `len` function 286 | 287 | The number of rows are returned when passing a pandas DataFrame to the builtin `len` function. We will make pandas_cub behave the same exact way. 288 | 289 | To do so we need to implement the special method `__len__`. This is what Python calls whenever an object is passed to the `len` function. 290 | 291 | Edit the `__len__` method and have it return the number of rows. Test with `test_len`. 292 | 293 | ### Special Methods 294 | 295 | Step 4 introduced us to the `__len__` 'special method'. Python has over 100 special methods that allow you to define how your class behaves when it interacts with a builtin function or operator. In the above example, if `df` is a DataFrame and a user calls `len(df)` then internally the `__len__` method will be called. All special methods begin and end with two underscores. 296 | 297 | Let's see a few more examples: 298 | 299 | * `df + 5` calls the `__add__` special method 300 | * `df > 5` calls the `__lt__` special method 301 | * `-df` calls the `__neg__` special method 302 | * `round(df)` calls the `__round__` special method 303 | 304 | We've actually already seen the special method `__init__` which is used to initialize an object and called when a user calls `DataFrame(data)`. 305 | 306 | The [Python documentation][10] has good (though complex) coverage of all the special methods. We will be implementing many more special methods. I strongly recommend to reference the documentation to learn more. 307 | 308 | ### 5. Return columns as a list 309 | 310 | In this step you will make `df.columns` return a list of the column names. Notice that `df.columns` is not a method here. There will be no parentheses that follow it. 311 | 312 | Looking at the source code, you will see that `columns` appears to be defined as if it is a method. But, directly above it is the property decorator. The `property` decorator will make `df.columns` work just like a method. 313 | 314 | Currently the keys in our `_data` dictionary refer to the columns in our DataFrame. Edit the `columns` 'method' (really a property) to return a list of the columns in order. Since we are working with Python 3.6, the dictionary keys are internally ordered. Take advantage of this. Validate with the `test_columns` test. 315 | 316 | ### The property decorator 317 | 318 | There is quite a bit more to the property decorator, including how its used to set attributes as is done in the next step. [This Stack Overflow question][11] contains a good examples that will explain more. 319 | 320 | ### 6. Set new column names 321 | 322 | In this step, we will be assigning all new columns to our DataFrame by setting the columns property equal to a list. A concrete example below shows how you would set new columns for a 3-column DataFrame. 323 | 324 | ```python 325 | df.columns = ['state', 'age', 'fruit'] 326 | ``` 327 | 328 | There are three parts to properties in Python; the getter, setter, and deleter. In the previous step, we defined the getter. In this step we will define the setter with the `columns.setter` decorator. The value on the right hand side of the assignment statement is passed to the method decorated by `columns.setter`. Edit this method and complete the following tasks: 329 | 330 | * Raise a `TypeError` if the object used to set new columns is not a list 331 | * Raise a `ValueError` if the number of column names in the list does not match the current DataFrame 332 | * Raise a `TypeError` if any of the columns are not strings 333 | * Raise a `ValueError` if any of the column names are duplicated in the list 334 | * Reassign the `_data` variable so that all the keys have been updated 335 | 336 | Test with `test_set_columns`. 337 | 338 | ### 7. The `shape` property 339 | 340 | The `shape` property will return a two-item tuple of the number of rows and columns. The property decorator is used again here so that `df.shape` can execute code like a method. We could just make it a normal method and invoke it with `df.shape()` but we are following pandas lead and keeping `shape` as a property. 341 | 342 | Test with `test_shape`. 343 | 344 | ### 8. Visual HTML representation in the notebook with the `_repr_html_` method 345 | 346 | Currently we have no representation of our DataFrame. If you try and output your DataFrame, you'll just get its location in memory and it will look something like this: 347 | 348 | ```python 349 | >>> df 350 | 351 | ``` 352 | 353 | The `_repr_html_` method is made available to developers by iPython so that your objects can have nicely formatted HTML displays within Jupyter Notebooks. Read more on this method [here in the iPython documentation][12] along with other similar methods for different representations. 354 | 355 | This method must return a string of html. This method is fairly complex and you must know some basic html to complete it. I recommend copying and pasting the implementation from pandas_cub_final instead of doing it yourself. 356 | 357 | If you do know HTML and are seeking a greater challenger use the docstrings to give you an idea of how the HTML may be formatted. There are no tests for this method. 358 | 359 | ### 9. The `values` property 360 | 361 | In pandas, `values` is a property that returns a single array of all the columns of data. Our DataFrame will do the same. Edit the `values` property and concatenate all the column arrays into a single two-dimensional numpy array. Return this array. The numpy `column_stack` function can be helpful here. 362 | 363 | Test with `test_values`. 364 | 365 | ### Hint when returning a DataFrame from a property/method 366 | 367 | Many of the next steps require you to return a DataFrame as the result of the property/method. To do so, you will use the DataFrame constructor like this. 368 | 369 | ```python 370 | return DataFrame(new_data) 371 | ``` 372 | 373 | where `new_data` is a dictionary mapping the column names to a one-dimensional numpy array. It is your job to create the `new_data` dictionary correctly. 374 | 375 | ### 10. The `dtypes` property 376 | 377 | The `dtypes` property will return a two-column DataFrame with the column names in the first column and their data type as a string in the other. Use 'Column Name' and 'Data Type' as column names. 378 | 379 | Use the `DTYPE_NAME` dictionary to convert from array `kind` to the string name of the data type. Test with `test_dtypes`. 380 | 381 | ### 11. Select a single column with the brackets 382 | 383 | In pandas, you can select a single column with `df['colname']`. Our DataFrame will do the same. To make an object work with the brackets, you must implement the `__getitem__` special method. See the [official documentation][13] for more. 384 | 385 | This special method is always passed a single parameter, the value within the brackets. We use `item` as the parameter name. 386 | 387 | In this step, use `isinstance` to check whether `item` is a string. If it is, return a one column DataFrame of that column. You will need to use the `DataFrame` constructor to return a DataFrame. 388 | 389 | These tests are under the `TestSelection` class. Run the `test_one_column` test. 390 | 391 | ### 12. Select multiple columns with a list 392 | 393 | Our DataFrame will also be able to select multiple columns if given a list within the brackets. For example, `df[['colname1', 'colname2']]` will return a two column DataFrame. 394 | 395 | Continue editing the `__getitem__` method. If `item` is a list, return a DataFrame of just those columns. Run `test_multiple_columns` to test. 396 | 397 | ### 13. Boolean Selection with a DataFrame 398 | 399 | In pandas, you can filter for specific rows of a DataFrame by passing in a boolean Series/array to the brackets. For instance, the following will select only the rows such that `a` is greater than 10. 400 | 401 | ```python 402 | >>> s = df['a'] > 10 403 | >>> df[s] 404 | ``` 405 | 406 | This is called boolean selection. We will make our DataFrame work similarly. Edit the `__getitem__` method and check whether `item` is a DataFrame. If it is then do the following: 407 | 408 | * If it is more than one column, raise a `ValueError` 409 | * Extract the underlying array from the single column 410 | * If the underlying array kind is not boolean ('b') raise a `ValueError` 411 | * Use the boolean array to return a new DataFrame with just the rows where the boolean array is `True` along with all the columns. 412 | 413 | Run `test_simple_boolean` to test 414 | 415 | ### (Optional) Simultaneous selection of rows and column 416 | 417 | Steps 14-18 are optional and fairly difficult. The outcome of these steps is to simultaneously select both rows and columns in the DataFrame. The syntax uses the brackets operator like the previous three steps and looks like this: 418 | 419 | ```python 420 | df[rs, cs] 421 | ``` 422 | 423 | where `rs` is the row selection and `cs` is the column selection. 424 | 425 | ### 14. (Optional) Check for simultaneous selection of rows and columns 426 | 427 | When you pass the brackets operator a sequence of comma separated values with `df[rs, cs]`, Python passes the `__getitem__` special method a tuple of all the values. 428 | 429 | To get started coding, within the `__getitem__` special method check whether `item` is a tuple instance. If is not, raise a `TypeError` and inform the user that they need to pass in either a string (step 11), a list of strings (step 12), a one column boolean DataFrame (step 13) or both a row and column selection (step 14). 430 | 431 | If `item` is a tuple, return the result of a call to the `_getitem_tuple` method. 432 | 433 | **Edit the `_getitem_tuple` method from now through step 18.** 434 | 435 | Within the `_getitem_tuple` method, raise a `ValueError` if it is not exactly two items in length. 436 | 437 | Run `test_simultaneous_tuple` to test. 438 | 439 | ### 15. (Optional) Select a single cell of data 440 | 441 | In this step, we will select a single cell of data with `df[rs, cs]`. We will assume `rs` is an integer and `cs` is either an integer or a string. 442 | 443 | To get started, assign the first element of `item` to the variable `row_selection` and the second element of `item` to `col_selection`. From step 14, we know that `item` must be a two-item tuple. 444 | 445 | If `row_selection` is an integer, reassign it as a one-element list of that integer. 446 | 447 | Check whether `col_selection` is an integer. If it is, reassign to a one-element list of the string column name it represents. 448 | 449 | If `col_selection` is a string, assign it to a one-element list of that string. 450 | 451 | Now both `row_selection` and `col_selection` are lists. You will return a single-row, single-column DataFrame. This is different than pandas, which just returns a scalar value. 452 | 453 | Write a for loop to iterate through each column in the `col_selection` list to create the `new_data` dictionary. Make sure to select just the row that is needed. 454 | 455 | This for-loop will be used for the steps through 18 to return the desired DataFrame. 456 | 457 | Run `test_single_element` to test. 458 | 459 | ### 16. (Optional) Simultaneously select rows as booleans, lists, or slices 460 | 461 | In this step, we will again be selecting rows and columns simultaneously with `df[rs, cs]`. We will allow `rs` to be either a single-column boolean DataFrame, a list of integers, or a slice. For now, `cs` will remain either an integer or a string. The following selections will be possible after this step. 462 | 463 | ```python 464 | df[df['a'] < 10, 'b'] 465 | df[[2, 4, 1], 'e'] 466 | df[2:5, 3] 467 | ``` 468 | 469 | If `row_selection` is a DataFrame, raise a `ValueError` if it is not one column. Reassign `row_selection` to the values (numpy array) of its column. Raise a `TypeError` if it is not a boolean data type. 470 | 471 | If `row_selection` is not a list or a slice raise a `TypeError` and inform the user that the row selection must be either an integer, list, slice, or DataFrame. You will not need to reassign `row_selection` for this case as it will select properly from a numpy array. 472 | 473 | Your for-loop from step 15 should return the DataFrame. 474 | 475 | Run `test_all_row_selections` to test. 476 | 477 | ### 17. (Optional) Simultaneous selection with multiple columns as a list 478 | 479 | The `row_selection` variable is now fully implemented. It can be either an integer, list of integers, a slice, or a one-column boolean DataFrame. 480 | 481 | As of now, the `col_selection` can only be an integer or a string. In this step, we will handle the case when it is a list. 482 | 483 | If `col_selection` is a list, create an empty list named `new_col_selection`. Iterate through each element of `col_selection` and check if it is an integer. If it is, append the string column name to `new_col_selection`. If not, assume it is a string and append it as it is to `new_col_selection`. 484 | 485 | `new_col_selection` will now be a list of string column names. Reassign `col_selection` to it. 486 | 487 | Again, your for-loop from step 15 will return the DataFrame. 488 | 489 | Run `test_list_columns` to test. 490 | 491 | ### 18. (Optional) Simultaneous selection with column slices 492 | 493 | In this step, we will allow our columns to be sliced with either strings or integers. The following selections will be acceptable. 494 | 495 | ```python 496 | df[rs, :3] 497 | df[rs, 1:10:2] 498 | df[rs, 'a':'f':2] 499 | ``` 500 | 501 | Where `rs` is any of the previously acceptable row selections. 502 | 503 | Check if `col_selection` is a slice. Slice objects have `start`, `stop`, and `step` attributes. Define new variables with the same name to hold those attributes of the slice object. 504 | 505 | If `col_selection` is not a slice raise a `TypeError` informing the user that the column selection must be an integer, string, list, or slice. 506 | 507 | If `start` is a string, reassign it to its integer index amongst the columns. 508 | 509 | If `stop` is a string, reassign it to its integer index amongst the columns **plus 1**. We add one here so that we include the last column. 510 | 511 | `start`, `stop`, and `step` should now be integers. Use them to reassign `col_selection` to a list of all the column names that are to be selected. You'll use slice notation to do this. 512 | 513 | The for-loop from 15 will still work to return the desired DataFrame. 514 | 515 | Run `test_col_slice` to test. 516 | 517 | ### 19. Tab Completion for column names 518 | 519 | It is possible to get help completing column names when doing single-column selections. For instance, let's say we had a column name called 'state' and began making a column selection with `df['s]`. If we press tab right here iPython can show us a dropdown list of all the column names beginning with 's'. 520 | 521 | We do this by returning the list of possible values we want to see from the `_ipython_key_completions_` method. Complete that method now. 522 | 523 | Run `test_tab_complete` to test. 524 | 525 | ### 20. Create a new column or overwrite an old column 526 | 527 | We will now have our DataFrame create a single new column or overwrite an existing one. Pandas allows for setting multiple columns at once, and even setting rows and columns simultaneously. Doing such is fairly complex and we will not implement those cases and instead focus on just single-column setting. 528 | 529 | Python allows setting via the brackets with the `__setitem__` special method. It receives two values when called, the `key` and the `value`. For instance, if we set a new column like this: 530 | 531 | ```python 532 | df['new col'] = np.array([10, 4, 99]) 533 | ``` 534 | 535 | the `key` would be 'new col' and the `value` would be the numpy array. 536 | 537 | If the `key` is not a string, raise a `NotImplementedError` stating that the DataFrame can only set a single column. 538 | 539 | If `value` is a numpy array, raise a `ValueError` if it is not 1D. Raise a different `ValueError` if the length is different than the calling DataFrame. 540 | 541 | If `value` is a DataFrame, raise a `ValueError` if it is not a single column. Raise a different `ValueError` if the length is different than the calling DataFrame. Reassign `value` to the underlying numpy array of the column. 542 | 543 | If `value` is a single integer, string, float, or boolean, use the numpy `repeat` function to reassign `value` to be an array the same length as the DataFrame with all values the same. For instance, the following should work. 544 | 545 | ```python 546 | >>> df['new col'] = 85 547 | ``` 548 | 549 | Raise a `TypeError` if `value` is not one of the above types. 550 | 551 | After completing the above, `value` will be a one-dimensional array. If it's data type `kind` is the string 'U', change its type to object. 552 | 553 | Finally, assign a new column by modifying the `_data` dictionary. 554 | 555 | Run `test_new_column` to test. 556 | 557 | ### 21. `head` and `tail` methods 558 | 559 | The `head` and `tail` methods each accept a single integer parameter `n` which is defaulted to 5. Have them return the first/last n rows. 560 | 561 | Run `test_head_tail` to complete this. 562 | 563 | ### 22. Generic aggregation methods 564 | 565 | We will now implement several methods that perform an aggregation. These methods all return a single value for each column. The following aggregation methods are defined. 566 | 567 | * min 568 | * max 569 | * mean 570 | * median 571 | * sum 572 | * var 573 | * std 574 | * all 575 | * any 576 | * argmax - index of the maximum 577 | * argmin - index of the minimum 578 | 579 | We will only be performing these aggregations column-wise and not row-wise. Pandas enables users to perform both row and column aggregations. 580 | 581 | If you look at our source code, you will see all of the aggregation methods already defined. You will not have to modify any of these methods individually. Instead, they all call the underlying `_agg` method passing it the numpy function. 582 | 583 | Complete the generic method `_agg` that accepts an aggregation function. 584 | 585 | Iterate through each column of your DataFrame and pass the underlying array to the aggregation function. Return a new DataFrame with the same number of columns, but with just a single row, the value of the aggregation. 586 | 587 | String columns with missing values raise a `TypeError`. Except this error and don't return columns where the aggregation cannot be found. 588 | 589 | Defining just the `_agg` method will make all the other aggregation methods work. 590 | 591 | All the aggregation methods have their own tests in a separate class named `TestAggregation`. They are all named similarly with 'test_' preceding the name of the aggregation. Run all the tests at once. 592 | 593 | ### 23. `isna` method 594 | 595 | The `isna` method will return a DataFrame the same shape as the original but with boolean values for every single value. Each value will be tested whether it is missing or not. Use `np.isnan` except in the case for strings which you can use a vectorized equality expression to `None`. 596 | 597 | Test with `test_isna` found in the `TestOtherMethods` class. 598 | 599 | ### 24. `count` method 600 | 601 | The `count` method returns a single-row DataFrame with the number of non-missing values for each column. You will want to use the result of `isna`. 602 | 603 | Test with `test_count` 604 | 605 | ### 25. `unique` method 606 | 607 | This method will return the unique values for each column in the DataFrame. Specifically, it will return a list of one-column DataFrames of unique values in each column. If there is a single column, just return the DataFrame. 608 | 609 | The reason we use a list of DataFrames is that each column may contain a different number of unique values. Use the `unique` numpy function. 610 | 611 | Test with `test_unique` 612 | 613 | ### 26. `nunique` method 614 | 615 | Return a single-row DataFrame with the number of unique values for each column. 616 | 617 | Test with `test_nunique` 618 | 619 | ### 27. `value_counts` method 620 | 621 | Return a list of DataFrames, unless there is just one column and then just return a single DataFrame. Each DataFrame will be two columns. The first column name will be the name of the original column. The second column name will be 'count'. The first column will contain the unique values in the original DataFrame column. The 'count' column will hold the frequency of each of those unique values. 622 | 623 | Use the numpy `unique` function with `return_counts` set to `True`. Return the DataFrames with sorted counts from greatest to least. Use the numpy `argsort` to help with this. 624 | 625 | Use the `test_value_counts` test within the `TestGrouping` class. 626 | 627 | ### 28. Normalize options for `value_counts` 628 | 629 | We will modify the `value_counts` method to return relative frequencies. The `value_counts` method also accepts a boolean parameter `normalize` that by default is set to `False`. If it is `True`, then return the relative frequencies of each value instead. 630 | 631 | Test with `test_value_counts_normalize` 632 | 633 | ### 29. `rename` method 634 | 635 | The `rename` method renames one or more column names. Accept a dictionary of old column names mapped to new column names. Return a DataFrame. Raise a `TypeError` if `columns` is not a dictionary. 636 | 637 | Test with`test_rename` within the `TestOtherMethods` class 638 | 639 | ### 30. `drop` method 640 | 641 | Accept a single string or a list of column names as strings. Return a DataFrame without those columns. Raise a `TypeError` if a string or list is not provided. 642 | 643 | Test with `test_drop` 644 | 645 | ### 31. Non-aggregation methods 646 | 647 | There are several non-aggregation methods that function similarly. All of the following non-aggregation methods return a DataFrame that is the same shape as the origin. 648 | 649 | * `abs` 650 | * `cummin` 651 | * `cummax` 652 | * `cumsum` 653 | * `clip` 654 | * `round` 655 | * `copy` 656 | 657 | All of the above methods will be implemented with the generic `_non_agg` method. This method is sent the numpy function name of the non-aggregating method. 658 | 659 | Pass only the boolean, integer, and float columns to this non-aggregating numpy function. 660 | 661 | Keep the string columns (only other data type) in your returned DataFrame. Use the `copy` array method to make an independent copy of them. 662 | 663 | Notice that some of these non-aggregating methods have extra keyword arguments. These are passed to `_non_agg` and collected with `**kwargs`. Make sure to pass them to the numpy function as well. 664 | 665 | There is a different test for each method in the `TestNonAgg` class. 666 | 667 | #### Update after videos 668 | 669 | If you are watching my videos for the course, I updated the pandas_cub_final init file to contain a better solution. The `round` method should ignore boolean columns. The original solution applied had each non-aggregation method work on boolean, integer, and float columns. 670 | 671 | ### 32. `diff` method 672 | 673 | The `diff` method accepts a single parameter `n` and takes the difference between the current row and the `n` previous row. For instance, if a column has the values [5, 10, 2] and `n=1`, the `diff` method would return [NaN, 5, -8]. The first value is missing because there is no value preceding it. 674 | 675 | The `diff` method is a non-aggregating method as well, but there is no direct numpy function that computes it. Instead, we will define a function within this method that computes this difference. 676 | 677 | Complete the body of the `func` function. 678 | 679 | Allow `n` to be either a negative or positive integer. You will have to set the first or last n values to `np.nan`. If you are doing this on an integer column, you will have to convert it to a float first as integer arrays cannot contain missing values. Use `np.roll` to help shift the data in the arrays. 680 | 681 | Test with `test_diff` 682 | 683 | ### 33. `pct_change` method 684 | 685 | The `pct_change` method is nearly identical to the `diff` method. The only difference is that this method returns the percentage change between the values and not the raw difference. Again, complete the body of the `func` function. 686 | 687 | Test with `test_pct_change` 688 | 689 | ### 34. Arithmetic and Comparison Operators 690 | 691 | All the common arithmetic and comparison operators will be made available to our DataFrame. For example, `df + 5` uses the plus operator to add 5 to each element of the DataFrame. Take a look at some of the following examples: 692 | 693 | ```python 694 | df + 5 695 | df - 5 696 | df > 5 697 | df != 5 698 | 5 + df 699 | 5 < df 700 | ``` 701 | 702 | All the arithmetic and comparison operators have corresponding special methods that are called whenever the operator is used. For instance `__add__` is called when the plus operator is used, and `__le__` is called whenever the less than or equal to operator is used. See [the full list][14] in the documentation. 703 | 704 | Each of these methods accepts a single parameter, which we have named `other`. All of these methods call a more generic `_oper` method which you will complete. 705 | 706 | Within the `_oper` method check if `other` is a DataFrame. Raise a `ValueError` if this DataFrame not one column. Otherwise, reassign `other` to be a 1D array of the values of its only column. 707 | 708 | If `other` is not a DataFrame do nothing and continue executing the rest of the method. We will not check directly if the types are compatible. Instead we will pass this task onto numpy. So, `df + 5` should work if all the columns in `df` are booleans, integers, or floats. 709 | 710 | Iterate through all the columns of your DataFrame and apply the operation to each array. You will need to use the `getattr` function along with the `op` string to retrieve the underlying numpy array method. For instance, `getattr(values, '__add__')` returns the method that uses the plus operator for the numpy array `values`. Return a new DataFrame with the operation applied to each column. 711 | 712 | Run all the tests in class `TestOperators` 713 | 714 | ### 35. `sort_values` method 715 | 716 | This method will sort the rows of the DataFrame by one or more columns. Allow the parameter `by` to be either a single column name as a string or a list of column names as strings. The DataFrame will be sorted by this column or columns. 717 | 718 | The second parameter, `asc`, will be a boolean controlling the direction of the sort. It is defaulted to `True` indicating that sorting will be ascending (lowest to greatest). Raise a `TypeError` if `by` is not a string or list. 719 | 720 | You will need to use numpy's `argsort` to get the order of the sort for a single column and `lexsort` to sort multiple columns. 721 | 722 | Run the following tests in the `TestMoreMethods` class. 723 | 724 | * `test_sort_values` 725 | * `test_sort_values_desc` 726 | * `test_sort_values_two` 727 | * `test_sort_values_two_desc` 728 | 729 | ### 36. `sample` method 730 | 731 | This method randomly samples the rows of the DataFrame. You can either choose an exact number to sample with `n` or a fraction with `frac`. Sample with replacement by using the boolean `replace`. The `seed` parameter will be used to set the random number seed. 732 | 733 | Raise a `ValueError` if `frac` is not positive and a `TypeError` if `n` is not an integer. 734 | 735 | You will be using numpy's random module to complete this method. Within it are the `seed` and `choice` functions. The latter function has a `replace` parameter that you will need to use. Return a new DataFrame with the new random rows. 736 | 737 | Run `test_sample` to test. 738 | 739 | ### 37. `pivot_table` method 740 | 741 | This is a complex method to implement. This method allows you to create a [pivot table][5] from your DataFrame. The following image shows the final result of calling the pivot table on a DataFrame. It summarizes the mean salary of each gender for each race. 742 | 743 | ![pt][6] 744 | 745 | A typical pivot table uses two columns as the **grouping columns** from your original DataFrame. The unique values of one of the grouping columns form a new column in the new DataFrame. In the example above, the race column had five unique values. 746 | 747 | The unique values of the other grouping column now form the columns of the new DataFrame. In the above example, there were two unique values of gender. 748 | 749 | In addition to the grouping columns is the **aggregating column**. This is typically a numeric column that will get summarized. In the above pivot table, the salary column was aggregated. 750 | 751 | The last component of a pivot table is the **aggregating function**. This determines how the aggregating columns get aggregated. Here, we used the `mean` function. 752 | 753 | The syntax used to produce the pivot table above is as follows: 754 | 755 | ```python 756 | df.pivot_table(rows='race', columns='gender', values='salary', aggfunc='mean') 757 | ``` 758 | 759 | `rows` and `columns` will be assigned the grouping columns. `values` will be assigned the aggregating column and `aggfunc` will be assigned the aggregating function. All four parameters will be strings. Since `aggfunc` is a string, you will need to use the builtin `getattr` function to get the correct numpy function. 760 | 761 | There are several approaches that you can take to implement this. One approach involves using a dictionary to store the unique combinations of the grouping columns as the keys and a list to store the values of the aggregative column. You could iterate over every single row and then use a two-item tuple to hold the values of the two grouping columns. A `defaultdict` from the collections module can help make this easier. Your dictionary would look something like this after you have iterated through the data. 762 | 763 | ```python 764 | {('black', 'male'): [50000, 90000, 40000], 765 | ('black', 'female'): [100000, 40000, 30000]} 766 | ``` 767 | 768 | Once you have mapped the groups to their respective values, you would need to iterate through this dictionary and apply the aggregation function to the values. Create a new dictionary for this. 769 | 770 | From here, you need to figure out how to turn this dictionary into the final DataFrame. You have all the values, you just need to create a dictionary of columns mapped to values. Use the first column as the unique values of the rows column. 771 | 772 | Other features: 773 | 774 | * Return a DataFrame that has the rows and columns sorted 775 | * You must make your pivot table work when passed just one of `rows` or `columns`. If just `rows` is passed return a two-column DataFrame with the first column containing the unique values of the rows and the second column containing the aggregations. Title the second column the same name as `aggfunc`. 776 | * If `aggfunc` is `None` and `values` is not None then raise a `ValueError`. 777 | * If `values` is `None` and `aggfunc` is not then raise a `ValueError` as there are no values to be aggregated. 778 | * If `aggfunc` and `values` are both `None` then set `aggfunc` equal to the string 'size'. This will produce a contingency table (the raw frequency of occurrence). You might need to create an empty numpy array to be a placeholder for the values. 779 | 780 | Run `test_pivot_table_rows_or_cols` and `test_pivot_table_both` in the `TestGrouping` class. 781 | 782 | ### 38. Automatically add documentation 783 | 784 | All docstrings can be retrieved programmitcally with the `__doc__` special attribute. Docstrings can also be dynamically set by assigning this same special attribute a string. 785 | 786 | This method is already completed and automatically adds documentation to the aggregation methods by setting the `__doc__` special attribute. 787 | 788 | ### 39. String-only methods with the `str` accessor 789 | 790 | Look back up at the `__init__` method. One of the last lines defines `str` as an instance variable assigned to a new instance of `StringMethods`. Pandas uses the same variable name for its DataFrames and calls it a string 'accessor'. We will also refer to it as an accessor as it gives us access to string-only methods. 791 | 792 | Scroll down below the definition of the `DataFrame` class. You will see the `StringMethods` class defined there. During initialization it stores a reference to the underlying DataFrame with `_df`. 793 | 794 | There are many string methods defined in this class. The first parameter to each string method is the name of the column you would like to apply the string method to. We will only allow our accessor to work on a single column of the DataFrame. 795 | 796 | You will only be modifying the `_str_method` which accepts the string method, the name of the column, and any extra arguments. 797 | 798 | Within `_str_method` select the underlying numpy array of the given `col`. Raise a `TypeError` if it does not have kind 'O'. 799 | 800 | Iterate over each value in the array and pass it to `method`. It will look like this: `method(val, *args)`. Return a one-column DataFrame with the new data. 801 | 802 | Test with class `TestStrings` 803 | 804 | ### 40. Reading simple CSVs 805 | 806 | It is important that our library be able to turn data in files into DataFrames. The `read_csv` function, at the very end of our module, will read in simple comma-separated value files (CSVs) and return a DataFrame. 807 | 808 | The `read_csv` function accepts a single parameter, `fn`, which is a string of the file name containing the data. Read through each line of the file. Assume the values in each line are separated by commas. Also assume the first line contains the column names. 809 | 810 | Create a dictionary to hold the data and return a new DataFrame. Use the file `employee.csv` in the `data` directory to test your function manually. 811 | 812 | Run all the tests in the `TestReadCSV` class. 813 | 814 | [0]: https://www.anaconda.com/distribution/ 815 | [1]: https://docs.pytest.org/en/latest/getting-started.html 816 | [2]: https://conda.io/projects/conda/en/latest/user-guide/tasks/manage-environments.html 817 | [3]: https://en.wikipedia.org/wiki/Test-driven_development 818 | [4]: https://docs.pytest.org/en/latest/goodpractices.html#conventions-for-python-test-discovery 819 | [5]: https://en.wikipedia.org/wiki/Pivot_table 820 | [6]: images/pivot.png 821 | [7]: https://numpydoc.readthedocs.io/en/latest/format.html 822 | [8]: https://docs.scipy.org/doc/numpy/reference/generated/numpy.dtype.kind.html#numpy.dtype.kind 823 | [9]: https://www.python.org/dev/peps/pep-0008/#id47 824 | [10]: https://docs.python.org/3/reference/datamodel.html#specialnames 825 | [11]: https://stackoverflow.com/questions/17330160/how-does-the-property-decorator-work 826 | [12]: https://ipython.readthedocs.io/en/stable/config/integrating.html 827 | [13]: https://docs.python.org/3/reference/datamodel.html 828 | [14]: https://docs.python.org/3/reference/datamodel.html#emulating-numeric-type 829 | [15]: https://www.youtube.com/watch?v=ZDa-Z5JzLYM 830 | [16]: https://docs.scipy.org/doc/numpy/user/quickstart.html 831 | [17]: images/exec_location.png 832 | [18]: https://jupyter-client.readthedocs.io/en/latest/kernels.html#making-kernels-for-jupyter 833 | [19]: https://ipython.readthedocs.io/en/stable/install/kernel_install.html 834 | [20]: images/change_kernel.png 835 | [21]: images/exec_location2.png 836 | [22]: https://www.dunderdata.com/offers/YpStPwfH/checkout 837 | -------------------------------------------------------------------------------- /data/employee.csv: -------------------------------------------------------------------------------- 1 | dept,race,gender,salary 2 | Houston Police Department-HPD,White,Male,45279 3 | Houston Fire Department (HFD),White,Male,63166 4 | Houston Police Department-HPD,Black,Male,66614 5 | Public Works & Engineering-PWE,Asian,Male,71680 6 | Houston Airport System (HAS),White,Male,42390 7 | Public Works & Engineering-PWE,White,Male,107962 8 | Houston Fire Department (HFD),Hispanic,Male,52644 9 | Health & Human Services,Black,Male,180416 10 | Public Works & Engineering-PWE,Black,Male,30347 11 | Health & Human Services,Black,Male,55269 12 | Houston Police Department-HPD,Black,Male,77076 13 | Houston Police Department-HPD,White,Male,81239 14 | Houston Police Department-HPD,White,Male,66614 15 | Public Works & Engineering-PWE,White,Male,61506 16 | Houston Fire Department (HFD),Black,Male,57815 17 | Public Works & Engineering-PWE,Black,Male,43264 18 | Houston Fire Department (HFD),White,Male,55437 19 | Houston Police Department-HPD,Black,Male,52514 20 | Houston Police Department-HPD,White,Male,61643 21 | Houston Police Department-HPD,White,Male,66614 22 | Houston Police Department-HPD,White,Male,81239 23 | Public Works & Engineering-PWE,Black,Male,29557 24 | Houston Fire Department (HFD),White,Male,62540 25 | Houston Fire Department (HFD),Hispanic,Male,61921 26 | Houston Police Department-HPD,White,Male,66614 27 | Health & Human Services,Black,Female,34923 28 | Public Works & Engineering-PWE,White,Female,60258 29 | Health & Human Services,Asian,Female,67499 30 | Public Works & Engineering-PWE,Black,Male,38168 31 | Houston Fire Department (HFD),Hispanic,Male,43528 32 | Houston Fire Department (HFD),White,Male,48190 33 | Houston Fire Department (HFD),White,Male,48190 34 | Houston Fire Department (HFD),Hispanic,Male,165216 35 | Houston Police Department-HPD,White,Male,45279 36 | Houston Police Department-HPD,White,Male,66614 37 | Public Works & Engineering-PWE,Black,Male,43597 38 | Public Works & Engineering-PWE,Black,Female,43618 39 | Houston Fire Department (HFD),White,Male,62540 40 | Houston Fire Department (HFD),White,Male,66523 41 | Houston Fire Department (HFD),Hispanic,Male,61921 42 | Houston Police Department-HPD,Hispanic,Male,81239 43 | Houston Fire Department (HFD),Hispanic,Male,55437 44 | Houston Airport System (HAS),Hispanic,Male,86297 45 | Houston Fire Department (HFD),White,Male,63166 46 | Houston Police Department-HPD,Black,Female,34757 47 | Public Works & Engineering-PWE,Hispanic,Male,49483 48 | Houston Airport System (HAS),White,Male,40144 49 | Houston Airport System (HAS),Black,Male,28163 50 | Public Works & Engineering-PWE,Hispanic,Male,30992 51 | Houston Fire Department (HFD),Black,Female,96668 52 | Houston Police Department-HPD,White,Male,81239 53 | Public Works & Engineering-PWE,Black,Female,26125 54 | Houston Airport System (HAS),White,Male,43826 55 | Health & Human Services,White,Female,100791 56 | Houston Police Department-HPD,Hispanic,Female,66614 57 | Houston Police Department-HPD,White,Male,66614 58 | Houston Police Department-HPD,Black,Male,66614 59 | Health & Human Services,Hispanic,Female,31387 60 | Houston Police Department-HPD,White,Male,66614 61 | Houston Airport System (HAS),White,Female,36296 62 | Houston Fire Department (HFD),White,Male,78355 63 | Houston Police Department-HPD,Hispanic,Male,52514 64 | Houston Police Department-HPD,Hispanic,Male,43443 65 | Public Works & Engineering-PWE,Black,Female,30347 66 | Public Works & Engineering-PWE,White,Male,44283 67 | Houston Fire Department (HFD),White,Male,62540 68 | Health & Human Services,Black,Female,48984 69 | Houston Fire Department (HFD),White,Male,62540 70 | Public Works & Engineering-PWE,Hispanic,Female,30347 71 | Public Works & Engineering-PWE,Black,Male,53373 72 | Houston Police Department-HPD,Hispanic,Male,66614 73 | Public Works & Engineering-PWE,Hispanic,Male,52156 74 | Parks & Recreation,Black,Female,26125 75 | Houston Fire Department (HFD),Hispanic,Male,66523 76 | Houston Police Department-HPD,Hispanic,Male,77076 77 | Houston Police Department-HPD,Native American,Male,60347 78 | Public Works & Engineering-PWE,Black,Male,29557 79 | Public Works & Engineering-PWE,Black,Male,30139 80 | Houston Airport System (HAS),Hispanic,Female,26125 81 | Houston Fire Department (HFD),White,Male,66523 82 | Houston Fire Department (HFD),Hispanic,Male,51194 83 | Public Works & Engineering-PWE,Black,Female,45926 84 | Houston Police Department-HPD,White,Male,66614 85 | Houston Fire Department (HFD),Black,Male,62540 86 | Houston Fire Department (HFD),White,Male,63166 87 | Health & Human Services,Hispanic,Female,36650 88 | Public Works & Engineering-PWE,Black,Male,41288 89 | Houston Fire Department (HFD),Hispanic,Male,51194 90 | Health & Human Services,Asian,Female,31554 91 | Houston Police Department-HPD,White,Male,91181 92 | Houston Police Department-HPD,Black,Female,66614 93 | Public Works & Engineering-PWE,Hispanic,Male,30597 94 | Health & Human Services,Black,Female,44200 95 | Houston Fire Department (HFD),Black,Male,40170 96 | Houston Fire Department (HFD),White,Male,62540 97 | Houston Police Department-HPD,White,Male,66614 98 | Health & Human Services,White,Male,59819 99 | Houston Police Department-HPD,Hispanic,Male,81239 100 | Houston Police Department-HPD,Hispanic,Female,43443 101 | Houston Police Department-HPD,White,Male,66614 102 | Houston Police Department-HPD,White,Male,81239 103 | Houston Fire Department (HFD),Hispanic,Male,61921 104 | Houston Police Department-HPD,White,Male,66614 105 | Public Works & Engineering-PWE,Black,Male,38646 106 | Houston Police Department-HPD,Black,Male,42000 107 | Parks & Recreation,Black,Male,33592 108 | Public Works & Engineering-PWE,Black,Male,52458 109 | Houston Police Department-HPD,Asian,Male,77076 110 | Houston Fire Department (HFD),Hispanic,Female,51194 111 | Houston Police Department-HPD,White,Female,81239 112 | Houston Police Department-HPD,Black,Female,36317 113 | Health & Human Services,Hispanic,Female,30826 114 | Houston Fire Department (HFD),Hispanic,Male,45791 115 | Houston Police Department-HPD,Black,Male,66614 116 | Houston Fire Department (HFD),White,Male,89590 117 | Houston Airport System (HAS),Black,Male,52499 118 | Public Works & Engineering-PWE,Black,Male,34798 119 | Public Works & Engineering-PWE,Black,Female,35942 120 | Houston Fire Department (HFD),Black,Male,52644 121 | Houston Police Department-HPD,White,Male,60347 122 | Houston Fire Department (HFD),White,Male,63166 123 | Houston Police Department-HPD,Hispanic,Male,55461 124 | Public Works & Engineering-PWE,Black,Male,31491 125 | Houston Police Department-HPD,White,Male,55461 126 | Houston Airport System (HAS),Black,Male,40435 127 | Public Works & Engineering-PWE,Black,Male,30555 128 | Houston Police Department-HPD,Asian,Male,62383 129 | Parks & Recreation,Black,Male,30368 130 | Public Works & Engineering-PWE,Black,Male,40435 131 | Houston Airport System (HAS),Black,Male,43826 132 | Houston Police Department-HPD,Black,Male,74511 133 | Houston Police Department-HPD,Hispanic,Male,47650 134 | Houston Police Department-HPD,Hispanic,Male,66614 135 | Parks & Recreation,Black,Male,29307 136 | Houston Police Department-HPD,Black,Male,81239 137 | Houston Fire Department (HFD),Hispanic,Male,43528 138 | Public Works & Engineering-PWE,Black,Male,56742 139 | Houston Police Department-HPD,White,Male,66614 140 | Houston Airport System (HAS),White,Male,120916 141 | Parks & Recreation,Hispanic,Male,30347 142 | Houston Police Department-HPD,White,Male,55461 143 | Houston Fire Department (HFD),Hispanic,Male,61921 144 | Houston Airport System (HAS),Black,Male,26125 145 | Houston Fire Department (HFD),White,Male,66523 146 | Houston Police Department-HPD,White,Male,66614 147 | Houston Fire Department (HFD),White,Male,210588 148 | Houston Police Department-HPD,Hispanic,Female,47650 149 | Houston Police Department-HPD,Hispanic,Male,59530 150 | Health & Human Services,Black,Female,45906 151 | Houston Police Department-HPD,Black,Male,47650 152 | Houston Police Department-HPD,White,Male,66614 153 | Public Works & Engineering-PWE,Asian,Male,30347 154 | Houston Police Department-HPD,White,Female,66614 155 | Public Works & Engineering-PWE,White,Male,110881 156 | Houston Police Department-HPD,Hispanic,Male,47650 157 | Houston Police Department-HPD,Hispanic,Female,30451 158 | Houston Fire Department (HFD),White,Male,57815 159 | Houston Airport System (HAS),Hispanic,Male,39707 160 | Public Works & Engineering-PWE,Hispanic,Male,28995 161 | Houston Airport System (HAS),White,Male,69514 162 | Public Works & Engineering-PWE,Black,Male,32344 163 | Public Works & Engineering-PWE,White,Male,30992 164 | Houston Police Department-HPD,Asian,Male,66614 165 | Houston Police Department-HPD,Black,Female,66614 166 | Health & Human Services,Black,Female,47382 167 | Public Works & Engineering-PWE,Black,Female,39104 168 | Houston Fire Department (HFD),Hispanic,Male,43528 169 | Houston Airport System (HAS),Asian,Male,62432 170 | Houston Fire Department (HFD),Hispanic,Male,57815 171 | Houston Police Department-HPD,Asian,Male,55461 172 | Health & Human Services,Hispanic,Female,43784 173 | Houston Police Department-HPD,Asian,Male,27914 174 | Houston Fire Department (HFD),White,Male,62540 175 | Public Works & Engineering-PWE,Hispanic,Male,53123 176 | Houston Police Department-HPD,Hispanic,Male,42370 177 | Houston Police Department-HPD,White,Male,66614 178 | Houston Police Department-HPD,White,Male,66614 179 | Houston Fire Department (HFD),Hispanic,Male,62540 180 | Public Works & Engineering-PWE,Black,Male,30347 181 | Houston Fire Department (HFD),Hispanic,Female,58040 182 | Houston Fire Department (HFD),White,Female,74270 183 | Houston Police Department-HPD,White,Female,46675 184 | Houston Police Department-HPD,Black,Female,59182 185 | Houston Police Department-HPD,White,Female,66614 186 | Public Works & Engineering-PWE,Black,Female,33987 187 | Houston Fire Department (HFD),Native American,Male,78355 188 | Public Works & Engineering-PWE,Hispanic,Male,47382 189 | Houston Police Department-HPD,White,Male,81239 190 | Houston Fire Department (HFD),White,Female,45791 191 | Houston Police Department-HPD,White,Male,66614 192 | Houston Police Department-HPD,Asian,Male,62338 193 | Houston Police Department-HPD,Black,Male,66614 194 | Public Works & Engineering-PWE,Black,Male,45490 195 | Houston Police Department-HPD,White,Male,66614 196 | Houston Fire Department (HFD),Hispanic,Male,66523 197 | Houston Fire Department (HFD),Hispanic,Male,40170 198 | Health & Human Services,Hispanic,Male,32802 199 | Houston Police Department-HPD,Hispanic,Male,47650 200 | Houston Police Department-HPD,Black,Male,42000 201 | Public Works & Engineering-PWE,Hispanic,Male,38542 202 | Houston Fire Department (HFD),White,Male,51194 203 | Public Works & Engineering-PWE,Black,Female,35485 204 | Houston Fire Department (HFD),Hispanic,Male,45791 205 | Houston Airport System (HAS),Black,Male,26125 206 | Houston Police Department-HPD,Black,Male,47650 207 | Houston Fire Department (HFD),White,Male,66523 208 | Houston Fire Department (HFD),White,Male,48190 209 | Houston Police Department-HPD,Black,Male,55461 210 | Health & Human Services,White,Female,81849 211 | Houston Police Department-HPD,Black,Female,55461 212 | Health & Human Services,Black,Female,30347 213 | Houston Airport System (HAS),Black,Male,35277 214 | Health & Human Services,Hispanic,Female,39312 215 | Houston Airport System (HAS),White,Male,58677 216 | Houston Fire Department (HFD),White,Male,62540 217 | Houston Fire Department (HFD),White,Male,61921 218 | Houston Fire Department (HFD),White,Male,66523 219 | Houston Police Department-HPD,Hispanic,Male,66614 220 | Health & Human Services,White,Female,34382 221 | Houston Fire Department (HFD),White,Male,51194 222 | Public Works & Engineering-PWE,Hispanic,Male,38438 223 | Houston Police Department-HPD,Black,Male,66614 224 | Houston Police Department-HPD,White,Male,43443 225 | Houston Police Department-HPD,Hispanic,Male,66614 226 | Houston Police Department-HPD,White,Male,52514 227 | Public Works & Engineering-PWE,White,Male,54600 228 | Public Works & Engineering-PWE,Black,Male,54995 229 | Public Works & Engineering-PWE,Hispanic,Male,51584 230 | Houston Fire Department (HFD),Black,Male,70181 231 | Public Works & Engineering-PWE,Black,Male,59748 232 | Public Works & Engineering-PWE,White,Male,141948 233 | Houston Airport System (HAS),Black,Male,100228 234 | Public Works & Engineering-PWE,Hispanic,Male,31450 235 | Public Works & Engineering-PWE,Black,Male,45989 236 | Health & Human Services,Black,Female,81972 237 | Parks & Recreation,Hispanic,Male,33634 238 | Houston Fire Department (HFD),White,Male,84995 239 | Health & Human Services,Hispanic,Female,28434 240 | Houston Police Department-HPD,Black,Male,66614 241 | Houston Police Department-HPD,Hispanic,Male,77076 242 | Houston Airport System (HAS),White,Female,51139 243 | Houston Police Department-HPD,Hispanic,Male,45279 244 | Public Works & Engineering-PWE,Black,Female,27373 245 | Houston Police Department-HPD,Black,Male,55461 246 | Houston Police Department-HPD,White,Male,91181 247 | Houston Police Department-HPD,White,Male,66614 248 | Houston Airport System (HAS),Hispanic,Female,68451 249 | Parks & Recreation,Hispanic,Female,36920 250 | Houston Police Department-HPD,White,Male,77076 251 | Houston Fire Department (HFD),Hispanic,Female,45791 252 | Houston Police Department-HPD,Black,Male,81239 253 | Houston Airport System (HAS),White,Male,65832 254 | Houston Police Department-HPD,Hispanic,Male,55461 255 | Houston Police Department-HPD,Black,Male,50621 256 | Houston Fire Department (HFD),Black,Male,52644 257 | Houston Fire Department (HFD),Hispanic,Female,40170 258 | Houston Fire Department (HFD),Hispanic,Male,51194 259 | Houston Fire Department (HFD),White,Female,63166 260 | Houston Fire Department (HFD),Hispanic,Male,48190 261 | Houston Police Department-HPD,White,Male,47650 262 | Houston Police Department-HPD,White,Female,60347 263 | Houston Fire Department (HFD),White,Female,40170 264 | Houston Police Department-HPD,Black,Male,61643 265 | Health & Human Services,Hispanic,Female,32864 266 | Public Works & Engineering-PWE,White,Male,56742 267 | Public Works & Engineering-PWE,Black,Male,41288 268 | Parks & Recreation,Black,Female,48090 269 | Houston Police Department-HPD,White,Male,60347 270 | Houston Police Department-HPD,Black,Male,66614 271 | Houston Fire Department (HFD),Black,Male,51194 272 | Houston Police Department-HPD,Asian,Male,61643 273 | Public Works & Engineering-PWE,Black,Male,30597 274 | Public Works & Engineering-PWE,Hispanic,Male,46800 275 | Houston Police Department-HPD,Hispanic,Male,50621 276 | Houston Fire Department (HFD),White,Male,61921 277 | Public Works & Engineering-PWE,White,Male,63315 278 | Houston Police Department-HPD,White,Male,66614 279 | Public Works & Engineering-PWE,Hispanic,Female,30347 280 | Public Works & Engineering-PWE,Black,Male,39978 281 | Houston Police Department-HPD,Hispanic,Female,47650 282 | Parks & Recreation,Black,Male,54600 283 | Houston Police Department-HPD,Black,Female,86534 284 | Houston Police Department-HPD,Hispanic,Female,37294 285 | Houston Fire Department (HFD),White,Male,62540 286 | Houston Police Department-HPD,White,Male,50621 287 | Houston Fire Department (HFD),Hispanic,Male,55437 288 | Houston Fire Department (HFD),Black,Male,66523 289 | Houston Police Department-HPD,Black,Male,45279 290 | Parks & Recreation,Black,Male,33488 291 | Houston Fire Department (HFD),White,Male,51194 292 | Public Works & Engineering-PWE,Black,Male,28995 293 | Public Works & Engineering-PWE,Asian,Female,90716 294 | Houston Airport System (HAS),Hispanic,Male,41808 295 | Health & Human Services,Asian,Female,94149 296 | Public Works & Engineering-PWE,White,Female,73783 297 | Houston Police Department-HPD,Black,Male,81239 298 | Houston Fire Department (HFD),Black,Male,55437 299 | Houston Police Department-HPD,Hispanic,Female,45279 300 | Houston Fire Department (HFD),Hispanic,Female,48190 301 | Houston Fire Department (HFD),Hispanic,Male,51194 302 | Public Works & Engineering-PWE,Black,Male,54600 303 | Houston Police Department-HPD,White,Male,55461 304 | Public Works & Engineering-PWE,White,Male,39478 305 | Houston Police Department-HPD,White,Male,61643 306 | Houston Police Department-HPD,Hispanic,Male,55461 307 | Public Works & Engineering-PWE,Black,Female,28600 308 | Public Works & Engineering-PWE,White,Male,44096 309 | Houston Police Department-HPD,Black,Male,81239 310 | Public Works & Engineering-PWE,Black,Female,60133 311 | Health & Human Services,White,Male,52072 312 | Houston Fire Department (HFD),White,Male,62540 313 | Public Works & Engineering-PWE,Asian,Female,51168 314 | Public Works & Engineering-PWE,Black,Male,32240 315 | Public Works & Engineering-PWE,Hispanic,Female,33488 316 | Houston Fire Department (HFD),White,Male,61921 317 | Houston Police Department-HPD,Black,Male,45279 318 | Houston Police Department-HPD,Asian,Male,50621 319 | Houston Fire Department (HFD),White,Male,62540 320 | Public Works & Engineering-PWE,Black,Male,53851 321 | Houston Police Department-HPD,White,Male,104455 322 | Houston Police Department-HPD,White,Male,55461 323 | Houston Fire Department (HFD),Black,Male,52644 324 | Houston Police Department-HPD,Black,Female,36317 325 | Houston Police Department-HPD,Black,Male,27893 326 | Public Works & Engineering-PWE,Black,Male,42266 327 | Houston Police Department-HPD,Black,Female,44970 328 | Houston Fire Department (HFD),White,Male,61226 329 | Public Works & Engineering-PWE,Black,Male,35797 330 | Houston Airport System (HAS),White,Female,88003 331 | Houston Fire Department (HFD),White,Male,52644 332 | Houston Police Department-HPD,Hispanic,Male,66614 333 | Houston Fire Department (HFD),White,Male,55437 334 | Health & Human Services,Black,Female,38189 335 | Houston Police Department-HPD,White,Male,61643 336 | Health & Human Services,Black,Female,76246 337 | Public Works & Engineering-PWE,Hispanic,Female,40352 338 | Houston Police Department-HPD,White,Male,66614 339 | Houston Fire Department (HFD),White,Male,70181 340 | Public Works & Engineering-PWE,Hispanic,Male,30597 341 | Public Works & Engineering-PWE,Black,Female,39666 342 | Houston Fire Department (HFD),White,Male,55437 343 | Houston Fire Department (HFD),White,Male,89590 344 | Health & Human Services,Hispanic,Female,30410 345 | Health & Human Services,Black,Female,30597 346 | Houston Airport System (HAS),White,Male,27851 347 | Houston Police Department-HPD,White,Male,66614 348 | Houston Fire Department (HFD),White,Male,55437 349 | Houston Police Department-HPD,Black,Female,24960 350 | Houston Police Department-HPD,Black,Male,47650 351 | Health & Human Services,Hispanic,Female,34299 352 | Health & Human Services,Black,Female,49379 353 | Public Works & Engineering-PWE,Black,Male,51605 354 | Public Works & Engineering-PWE,Black,Male,32968 355 | Houston Airport System (HAS),Hispanic,Female,26125 356 | Houston Police Department-HPD,Black,Female,66614 357 | Houston Police Department-HPD,Black,Male,60347 358 | Public Works & Engineering-PWE,Black,Female,35485 359 | Houston Fire Department (HFD),White,Male,62540 360 | Public Works & Engineering-PWE,Black,Female,30347 361 | Health & Human Services,Black,Female,31470 362 | Houston Police Department-HPD,White,Female,60347 363 | Public Works & Engineering-PWE,White,Male,63461 364 | Houston Police Department-HPD,Asian,Female,91181 365 | Public Works & Engineering-PWE,Black,Female,70780 366 | Houston Police Department-HPD,Asian,Male,81239 367 | Houston Police Department-HPD,Hispanic,Male,61643 368 | Houston Police Department-HPD,Black,Female,35318 369 | Houston Fire Department (HFD),White,Male,40170 370 | Houston Police Department-HPD,Hispanic,Male,50621 371 | Health & Human Services,Hispanic,Female,29245 372 | Houston Police Department-HPD,White,Male,81239 373 | Public Works & Engineering-PWE,Black,Male,83744 374 | Parks & Recreation,Black,Female,56826 375 | Houston Fire Department (HFD),White,Male,61226 376 | Public Works & Engineering-PWE,Hispanic,Male,26104 377 | Public Works & Engineering-PWE,White,Male,59613 378 | Houston Police Department-HPD,White,Male,66614 379 | Houston Police Department-HPD,White,Male,66614 380 | Houston Fire Department (HFD),White,Male,43528 381 | Houston Fire Department (HFD),White,Male,61921 382 | Public Works & Engineering-PWE,White,Male,93089 383 | Public Works & Engineering-PWE,Asian,Male,64432 384 | Public Works & Engineering-PWE,White,Female,53726 385 | Houston Police Department-HPD,White,Male,66614 386 | Public Works & Engineering-PWE,White,Female,34029 387 | Health & Human Services,Hispanic,Female,28600 388 | Houston Police Department-HPD,Hispanic,Male,66614 389 | Houston Police Department-HPD,Hispanic,Male,61643 390 | Houston Police Department-HPD,White,Male,66614 391 | Houston Police Department-HPD,Black,Female,81239 392 | Houston Police Department-HPD,Hispanic,Male,66614 393 | Houston Fire Department (HFD),Hispanic,Male,48190 394 | Houston Police Department-HPD,White,Male,81239 395 | Public Works & Engineering-PWE,Black,Male,30992 396 | Parks & Recreation,Black,Male,35027 397 | Houston Police Department-HPD,Hispanic,Female,60347 398 | Houston Airport System (HAS),Black,Female,110686 399 | Houston Police Department-HPD,White,Male,66614 400 | Public Works & Engineering-PWE,Black,Female,45157 401 | Houston Police Department-HPD,Black,Female,52514 402 | Houston Police Department-HPD,White,Male,43443 403 | Houston Fire Department (HFD),Black,Male,28024 404 | Houston Police Department-HPD,White,Female,66614 405 | Public Works & Engineering-PWE,Black,Male,36130 406 | Houston Police Department-HPD,White,Male,47650 407 | Houston Fire Department (HFD),Hispanic,Male,66523 408 | Houston Airport System (HAS),Black,Male,43826 409 | Houston Police Department-HPD,Hispanic,Female,43443 410 | Houston Fire Department (HFD),White,Male,61226 411 | Public Works & Engineering-PWE,White,Male,54600 412 | Houston Fire Department (HFD),White,Male,61226 413 | Public Works & Engineering-PWE,Black,Male,34861 414 | Public Works & Engineering-PWE,Black,Female,56992 415 | Houston Fire Department (HFD),White,Male,45791 416 | Houston Police Department-HPD,White,Male,50621 417 | Houston Police Department-HPD,Black,Male,47650 418 | Parks & Recreation,Hispanic,Male,26125 419 | Health & Human Services,Hispanic,Female,38709 420 | Public Works & Engineering-PWE,Black,Male,34320 421 | Houston Police Department-HPD,Black,Male,35838 422 | Health & Human Services,Black,Male,60496 423 | Houston Police Department-HPD,Hispanic,Male,55461 424 | Public Works & Engineering-PWE,White,Male,90716 425 | Public Works & Engineering-PWE,Asian,Male,55695 426 | Houston Fire Department (HFD),Hispanic,Male,55437 427 | Houston Police Department-HPD,Black,Female,60347 428 | Houston Police Department-HPD,Black,Male,61643 429 | Houston Police Department-HPD,White,Male,66614 430 | Houston Police Department-HPD,White,Female,61643 431 | Houston Police Department-HPD,White,Female,81239 432 | Public Works & Engineering-PWE,Black,Female,40851 433 | Houston Fire Department (HFD),White,Male,61226 434 | Public Works & Engineering-PWE,Black,Female,30347 435 | Houston Fire Department (HFD),Hispanic,Male,61921 436 | Public Works & Engineering-PWE,Black,Male,45926 437 | Public Works & Engineering-PWE,White,Male,56576 438 | Public Works & Engineering-PWE,White,Male,58157 439 | Houston Police Department-HPD,Black,Female,55461 440 | Houston Police Department-HPD,White,Male,55461 441 | Public Works & Engineering-PWE,Black,Female,51563 442 | Houston Fire Department (HFD),White,Male,61921 443 | Public Works & Engineering-PWE,Black,Female,52957 444 | Houston Airport System (HAS),Asian,Male,37003 445 | Houston Police Department-HPD,Hispanic,Male,61643 446 | Public Works & Engineering-PWE,White,Male,40622 447 | Houston Police Department-HPD,White,Female,66614 448 | Parks & Recreation,Hispanic,Male,36920 449 | Houston Fire Department (HFD),Black,Male,61226 450 | Public Works & Engineering-PWE,White,Female,105503 451 | Houston Police Department-HPD,White,Female,99953 452 | Public Works & Engineering-PWE,Hispanic,Male,63338 453 | Public Works & Engineering-PWE,White,Female,90715 454 | Houston Police Department-HPD,White,Male,60347 455 | Public Works & Engineering-PWE,Black,Female,33488 456 | Houston Police Department-HPD,White,Female,35318 457 | Houston Airport System (HAS),Hispanic,Male,35318 458 | Houston Police Department-HPD,White,Male,74511 459 | Houston Airport System (HAS),Black,Female,150416 460 | Houston Police Department-HPD,Hispanic,Male,66614 461 | Houston Police Department-HPD,Asian,Male,61643 462 | Houston Fire Department (HFD),Asian,Male,43528 463 | Houston Police Department-HPD,Hispanic,Female,47650 464 | Health & Human Services,Black,Female,45614 465 | Houston Fire Department (HFD),White,Male,51194 466 | Houston Fire Department (HFD),White,Male,63166 467 | Houston Police Department-HPD,Hispanic,Male,61643 468 | Houston Police Department-HPD,White,Male,66614 469 | Houston Police Department-HPD,Black,Male,55461 470 | Houston Police Department-HPD,Black,Female,29827 471 | Houston Airport System (HAS),Black,Female,26125 472 | Public Works & Engineering-PWE,White,Male,29765 473 | Parks & Recreation,Hispanic,Male,29910 474 | Parks & Recreation,Black,Male,26125 475 | Houston Police Department-HPD,Black,Male,42000 476 | Public Works & Engineering-PWE,Black,Female,32406 477 | Health & Human Services,Black,Female,31387 478 | Houston Fire Department (HFD),White,Male,70181 479 | Public Works & Engineering-PWE,Black,Female,27373 480 | Houston Police Department-HPD,Hispanic,Male,81239 481 | Houston Fire Department (HFD),Hispanic,Male,61226 482 | Houston Police Department-HPD,Hispanic,Male,50621 483 | Public Works & Engineering-PWE,Hispanic,Male,31158 484 | Public Works & Engineering-PWE,Hispanic,Male,84500 485 | Houston Police Department-HPD,Black,Male,41621 486 | Houston Fire Department (HFD),White,Male,40170 487 | Health & Human Services,Hispanic,Male,31803 488 | Houston Fire Department (HFD),Hispanic,Male,57815 489 | Health & Human Services,Asian,Male,46010 490 | Health & Human Services,White,Male,47362 491 | Public Works & Engineering-PWE,Asian,Female,95950 492 | Houston Fire Department (HFD),Hispanic,Male,78355 493 | Houston Police Department-HPD,White,Male,45279 494 | Public Works & Engineering-PWE,Black,Female,29931 495 | Houston Police Department-HPD,Black,Male,66614 496 | Public Works & Engineering-PWE,Black,Female,34757 497 | Parks & Recreation,Hispanic,Female,42640 498 | Parks & Recreation,Black,Female,57533 499 | Houston Police Department-HPD,White,Male,66614 500 | Public Works & Engineering-PWE,Black,Female,30347 501 | Houston Police Department-HPD,Black,Female,32261 502 | Houston Police Department-HPD,Hispanic,Female,47650 503 | Houston Police Department-HPD,Hispanic,Male,66614 504 | Houston Fire Department (HFD),White,Male,70181 505 | Health & Human Services,Black,Female,63785 506 | Parks & Recreation,Black,Male,28205 507 | Houston Fire Department (HFD),White,Male,45791 508 | Houston Police Department-HPD,White,Male,66614 509 | Houston Fire Department (HFD),Hispanic,Male,40170 510 | Houston Police Department-HPD,Hispanic,Male,60347 511 | Public Works & Engineering-PWE,White,Female,98752 512 | Public Works & Engineering-PWE,Black,Female,32968 513 | Houston Fire Department (HFD),Hispanic,Male,45791 514 | Houston Police Department-HPD,White,Male,47650 515 | Houston Fire Department (HFD),White,Male,62540 516 | Public Works & Engineering-PWE,Black,Male,34653 517 | Houston Fire Department (HFD),White,Male,45791 518 | Public Works & Engineering-PWE,Black,Male,32968 519 | Houston Police Department-HPD,Hispanic,Male,47650 520 | Houston Police Department-HPD,Black,Male,66614 521 | Houston Police Department-HPD,White,Male,60347 522 | Houston Police Department-HPD,Hispanic,Female,66614 523 | Public Works & Engineering-PWE,Hispanic,Male,36920 524 | Houston Police Department-HPD,White,Male,66614 525 | Houston Police Department-HPD,Hispanic,Male,50621 526 | Public Works & Engineering-PWE,Hispanic,Female,44866 527 | Houston Police Department-HPD,Black,Male,66614 528 | Public Works & Engineering-PWE,Hispanic,Male,53581 529 | Houston Police Department-HPD,White,Male,43443 530 | Houston Airport System (HAS),Black,Female,39229 531 | Houston Police Department-HPD,Black,Female,29890 532 | Houston Police Department-HPD,Black,Male,81239 533 | Houston Fire Department (HFD),White,Male,61921 534 | Houston Fire Department (HFD),White,Male,66523 535 | Houston Airport System (HAS),Hispanic,Male,33238 536 | Public Works & Engineering-PWE,Asian,Male,34611 537 | Houston Fire Department (HFD),Hispanic,Female,41267 538 | Houston Police Department-HPD,White,Male,55461 539 | Houston Fire Department (HFD),White,Male,70181 540 | Houston Police Department-HPD,Black,Male,43035 541 | Houston Police Department-HPD,Asian,Female,35152 542 | Houston Fire Department (HFD),Hispanic,Male,70181 543 | Houston Fire Department (HFD),White,Male,70181 544 | Houston Fire Department (HFD),Black,Male,66523 545 | Houston Fire Department (HFD),Black,Male,130585 546 | Houston Police Department-HPD,Black,Female,66614 547 | Houston Fire Department (HFD),Hispanic,Female,66523 548 | Houston Police Department-HPD,White,Male,28169 549 | Public Works & Engineering-PWE,Black,Male,42182 550 | Public Works & Engineering-PWE,Black,Male,29557 551 | Houston Fire Department (HFD),White,Male,52644 552 | Houston Police Department-HPD,Black,Male,66614 553 | Public Works & Engineering-PWE,Black,Female,48360 554 | Houston Police Department-HPD,Asian,Male,163228 555 | Houston Airport System (HAS),Hispanic,Male,39125 556 | Houston Fire Department (HFD),Black,Male,51194 557 | Houston Police Department-HPD,Black,Female,58409 558 | Houston Fire Department (HFD),White,Male,66523 559 | Houston Fire Department (HFD),White,Male,61921 560 | Public Works & Engineering-PWE,Black,Female,37440 561 | Houston Police Department-HPD,Hispanic,Male,66614 562 | Houston Police Department-HPD,Hispanic,Male,52514 563 | Houston Airport System (HAS),White,Male,96284 564 | Public Works & Engineering-PWE,Hispanic,Male,90293 565 | Houston Fire Department (HFD),Hispanic,Male,48190 566 | Houston Fire Department (HFD),White,Male,62540 567 | Health & Human Services,Black,Female,57433 568 | Houston Fire Department (HFD),White,Male,51194 569 | Houston Police Department-HPD,White,Male,55461 570 | Public Works & Engineering-PWE,Asian,Male,31408 571 | Parks & Recreation,Hispanic,Male,46580 572 | Public Works & Engineering-PWE,Black,Female,40581 573 | Houston Police Department-HPD,White,Female,45552 574 | Houston Police Department-HPD,Black,Male,47650 575 | Houston Police Department-HPD,White,Female,50621 576 | Houston Fire Department (HFD),Black,Male,43528 577 | Houston Fire Department (HFD),White,Male,55437 578 | Houston Police Department-HPD,White,Male,81239 579 | Houston Police Department-HPD,White,Male,60347 580 | Health & Human Services,Hispanic,Female,37835 581 | Public Works & Engineering-PWE,Black,Male,30139 582 | Houston Police Department-HPD,Black,Male,81239 583 | Public Works & Engineering-PWE,Asian,Male,50960 584 | Health & Human Services,Hispanic,Female,34008 585 | Houston Fire Department (HFD),Hispanic,Male,55437 586 | Houston Police Department-HPD,Hispanic,Female,27914 587 | Health & Human Services,Hispanic,Female,32198 588 | Houston Police Department-HPD,Hispanic,Male,81239 589 | Houston Fire Department (HFD),Black,Male,40170 590 | Houston Police Department-HPD,White,Male,199596 591 | Public Works & Engineering-PWE,White,Male,57304 592 | Houston Police Department-HPD,White,Male,66614 593 | Parks & Recreation,Hispanic,Male,26125 594 | Houston Police Department-HPD,Asian,Female,35880 595 | Public Works & Engineering-PWE,Black,Male,31762 596 | Houston Police Department-HPD,Hispanic,Male,42000 597 | Public Works & Engineering-PWE,Black,Male,27373 598 | Parks & Recreation,Black,Female,31075 599 | Houston Police Department-HPD,Hispanic,Male,81239 600 | Houston Fire Department (HFD),Black,Male,43528 601 | Health & Human Services,Black,Female,34320 602 | Parks & Recreation,Black,Male,36920 603 | Houston Fire Department (HFD),White,Male,78355 604 | Houston Fire Department (HFD),White,Male,66523 605 | Public Works & Engineering-PWE,Black,Male,41288 606 | Houston Fire Department (HFD),Black,Male,70181 607 | Houston Police Department-HPD,Hispanic,Male,74511 608 | Houston Fire Department (HFD),White,Male,45791 609 | Public Works & Engineering-PWE,Black,Male,28995 610 | Public Works & Engineering-PWE,White,Male,63336 611 | Houston Airport System (HAS),White,Female,100873 612 | Houston Police Department-HPD,Black,Male,66614 613 | Houston Police Department-HPD,Asian,Male,81239 614 | Houston Police Department-HPD,Hispanic,Male,91181 615 | Houston Police Department-HPD,Hispanic,Female,60347 616 | Public Works & Engineering-PWE,White,Male,31429 617 | Houston Fire Department (HFD),White,Male,89590 618 | Houston Fire Department (HFD),White,Male,78355 619 | Houston Police Department-HPD,Hispanic,Female,55461 620 | Houston Fire Department (HFD),Black,Male,61226 621 | Houston Fire Department (HFD),White,Male,99480 622 | Houston Police Department-HPD,White,Male,66614 623 | Houston Police Department-HPD,Asian,Female,66614 624 | Public Works & Engineering-PWE,Black,Male,36837 625 | Houston Fire Department (HFD),Black,Male,61226 626 | Health & Human Services,Black,Female,83174 627 | Houston Fire Department (HFD),Hispanic,Male,61226 628 | Houston Fire Department (HFD),Black,Male,48190 629 | Houston Fire Department (HFD),Black,Male,43528 630 | Houston Police Department-HPD,Asian,Male,66614 631 | Houston Police Department-HPD,White,Male,55461 632 | Houston Fire Department (HFD),White,Male,51194 633 | Houston Fire Department (HFD),White,Male,40170 634 | Health & Human Services,Hispanic,Female,65589 635 | Houston Police Department-HPD,White,Male,43443 636 | Houston Police Department-HPD,White,Male,66614 637 | Houston Police Department-HPD,Hispanic,Female,33322 638 | Houston Police Department-HPD,Hispanic,Female,47008 639 | Houston Fire Department (HFD),Black,Male,70181 640 | Houston Police Department-HPD,Black,Female,32490 641 | Parks & Recreation,White,Male,37565 642 | Houston Police Department-HPD,White,Male,86534 643 | Health & Human Services,Black,Female,82915 644 | Houston Police Department-HPD,White,Male,66614 645 | Houston Fire Department (HFD),Hispanic,Male,51194 646 | Houston Police Department-HPD,Black,Male,61643 647 | Public Works & Engineering-PWE,Hispanic,Male,40518 648 | Houston Fire Department (HFD),Hispanic,Male,40170 649 | Public Works & Engineering-PWE,Asian,Male,79953 650 | Houston Police Department-HPD,White,Male,66614 651 | Public Works & Engineering-PWE,Hispanic,Male,46238 652 | Houston Fire Department (HFD),White,Male,70181 653 | Health & Human Services,Black,Female,46384 654 | Public Works & Engineering-PWE,Black,Male,30597 655 | Houston Airport System (HAS),Black,Male,186192 656 | Houston Police Department-HPD,Hispanic,Male,66614 657 | Houston Police Department-HPD,Asian,Male,28169 658 | Houston Fire Department (HFD),Hispanic,Male,43528 659 | Houston Police Department-HPD,Hispanic,Female,43035 660 | Public Works & Engineering-PWE,White,Male,46550 661 | Houston Fire Department (HFD),Hispanic,Male,55437 662 | Houston Fire Department (HFD),White,Male,70181 663 | Houston Police Department-HPD,Hispanic,Male,66614 664 | Houston Police Department-HPD,Native American,Male,81239 665 | Houston Fire Department (HFD),White,Male,45791 666 | Public Works & Engineering-PWE,White,Female,70565 667 | Houston Police Department-HPD,Hispanic,Male,81239 668 | Houston Police Department-HPD,White,Male,66614 669 | Houston Police Department-HPD,White,Male,55461 670 | Houston Fire Department (HFD),White,Male,62540 671 | Houston Police Department-HPD,White,Male,66614 672 | Houston Police Department-HPD,Black,Male,66614 673 | Houston Police Department-HPD,Asian,Male,50378 674 | Houston Police Department-HPD,Hispanic,Male,81239 675 | Houston Fire Department (HFD),White,Male,55437 676 | Houston Fire Department (HFD),White,Male,61921 677 | Public Works & Engineering-PWE,Hispanic,Male,30139 678 | Public Works & Engineering-PWE,Black,Male,66310 679 | Houston Police Department-HPD,White,Male,69661 680 | Houston Police Department-HPD,Black,Male,66614 681 | Houston Fire Department (HFD),White,Male,70181 682 | Health & Human Services,Black,Female,31782 683 | Houston Police Department-HPD,Black,Female,66614 684 | Public Works & Engineering-PWE,White,Male,86288 685 | Public Works & Engineering-PWE,Asian,Male,87236 686 | Houston Fire Department (HFD),White,Male,63166 687 | Houston Airport System (HAS),Hispanic,Female,29640 688 | Houston Police Department-HPD,Black,Male,47650 689 | Public Works & Engineering-PWE,White,Male,105354 690 | Houston Airport System (HAS),Black,Male,47237 691 | Houston Police Department-HPD,White,Male,66614 692 | Houston Police Department-HPD,Black,Female,34528 693 | Parks & Recreation,Black,Male,50028 694 | Houston Airport System (HAS),Black,Male,48568 695 | Houston Police Department-HPD,White,Male,81239 696 | Houston Police Department-HPD,Black,Female,26125 697 | Public Works & Engineering-PWE,Black,Male,34320 698 | Houston Police Department-HPD,Hispanic,Male,55461 699 | Houston Airport System (HAS),Black,Male,62650 700 | Public Works & Engineering-PWE,Black,Male,32968 701 | Public Works & Engineering-PWE,Asian,Male,44096 702 | Public Works & Engineering-PWE,Hispanic,Male,54600 703 | Public Works & Engineering-PWE,Hispanic,Female,45178 704 | Health & Human Services,Hispanic,Female,40976 705 | Health & Human Services,Black,Female,70762 706 | Houston Fire Department (HFD),White,Male,51194 707 | Houston Fire Department (HFD),White,Male,43528 708 | Houston Police Department-HPD,White,Male,66614 709 | Houston Police Department-HPD,Black,Female,55461 710 | Houston Police Department-HPD,Hispanic,Male,66614 711 | Public Works & Engineering-PWE,Black,Male,36213 712 | Houston Police Department-HPD,Black,Male,77076 713 | Public Works & Engineering-PWE,Black,Male,71170 714 | Health & Human Services,Asian,Male,52624 715 | Public Works & Engineering-PWE,White,Male,37211 716 | Houston Police Department-HPD,Black,Female,37336 717 | Houston Police Department-HPD,Black,Male,66614 718 | Houston Police Department-HPD,Black,Male,60653 719 | Houston Police Department-HPD,Black,Female,81239 720 | Houston Fire Department (HFD),Black,Male,43528 721 | Health & Human Services,Black,Female,40144 722 | Houston Police Department-HPD,White,Male,42000 723 | Houston Fire Department (HFD),Hispanic,Male,70181 724 | Houston Police Department-HPD,White,Male,61643 725 | Houston Police Department-HPD,Hispanic,Male,47650 726 | Houston Police Department-HPD,Asian,Male,66614 727 | Public Works & Engineering-PWE,Hispanic,Female,66449 728 | Houston Fire Department (HFD),White,Male,74270 729 | Houston Police Department-HPD,Asian,Male,45279 730 | Houston Airport System (HAS),Black,Male,27082 731 | Health & Human Services,White,Male,47632 732 | Houston Fire Department (HFD),White,Male,43528 733 | Houston Police Department-HPD,White,Female,91181 734 | Public Works & Engineering-PWE,Black,Male,42640 735 | Public Works & Engineering-PWE,Hispanic,Female,62426 736 | Houston Police Department-HPD,Hispanic,Male,66614 737 | Houston Police Department-HPD,White,Male,81239 738 | Public Works & Engineering-PWE,Black,Male,44429 739 | Houston Police Department-HPD,Asian,Male,52514 740 | Houston Police Department-HPD,Hispanic,Male,66614 741 | Public Works & Engineering-PWE,Black,Female,86763 742 | Public Works & Engineering-PWE,Asian,Male,47382 743 | Houston Police Department-HPD,Black,Female,31907 744 | Health & Human Services,Black,Female,48027 745 | Houston Fire Department (HFD),White,Male,62540 746 | Houston Police Department-HPD,Hispanic,Male,60347 747 | Public Works & Engineering-PWE,Black,Male,54995 748 | Houston Fire Department (HFD),White,Male,70181 749 | Houston Police Department-HPD,White,Male,30888 750 | Houston Police Department-HPD,Black,Male,60347 751 | Houston Fire Department (HFD),Black,Male,63166 752 | Houston Police Department-HPD,Hispanic,Male,66614 753 | Health & Human Services,Hispanic,Male,46363 754 | Houston Police Department-HPD,White,Male,81239 755 | Houston Fire Department (HFD),Hispanic,Male,52644 756 | Houston Police Department-HPD,White,Male,66614 757 | Houston Police Department-HPD,Hispanic,Male,66614 758 | Public Works & Engineering-PWE,Hispanic,Male,57470 759 | Parks & Recreation,Black,Male,56014 760 | Public Works & Engineering-PWE,Black,Female,77899 761 | Houston Police Department-HPD,Black,Male,77076 762 | Houston Police Department-HPD,Hispanic,Female,26125 763 | Houston Fire Department (HFD),Black,Male,62540 764 | Houston Police Department-HPD,Black,Female,31845 765 | Public Works & Engineering-PWE,Black,Male,39666 766 | Health & Human Services,Black,Male,38709 767 | Houston Police Department-HPD,Hispanic,Male,66614 768 | Houston Airport System (HAS),White,Female,35256 769 | Houston Police Department-HPD,White,Male,81239 770 | Public Works & Engineering-PWE,White,Male,34632 771 | Public Works & Engineering-PWE,Black,Male,37128 772 | Houston Police Department-HPD,White,Male,66614 773 | Health & Human Services,Black,Male,53832 774 | Houston Police Department-HPD,White,Male,55461 775 | Houston Police Department-HPD,Black,Female,30888 776 | Houston Fire Department (HFD),White,Male,63166 777 | Houston Police Department-HPD,Black,Male,66614 778 | Houston Fire Department (HFD),White,Male,63166 779 | Houston Airport System (HAS),White,Male,51501 780 | Houston Airport System (HAS),Hispanic,Male,31158 781 | Houston Fire Department (HFD),White,Male,61226 782 | Houston Police Department-HPD,Asian,Male,61643 783 | Public Works & Engineering-PWE,Hispanic,Male,41683 784 | Parks & Recreation,Black,Male,30347 785 | Houston Police Department-HPD,White,Female,66614 786 | Houston Police Department-HPD,White,Male,66614 787 | Houston Police Department-HPD,White,Male,55453 788 | Houston Fire Department (HFD),White,Male,78355 789 | Health & Human Services,White,Male,120799 790 | Houston Police Department-HPD,Hispanic,Male,43443 791 | Parks & Recreation,Black,Male,28766 792 | Houston Fire Department (HFD),White,Male,89590 793 | Houston Police Department-HPD,Hispanic,Female,66614 794 | Houston Fire Department (HFD),White,Male,74270 795 | Houston Fire Department (HFD),Hispanic,Male,57815 796 | Houston Police Department-HPD,White,Male,66614 797 | Houston Police Department-HPD,Hispanic,Female,55461 798 | Public Works & Engineering-PWE,Hispanic,Male,53435 799 | Health & Human Services,Native American,Female,49379 800 | Public Works & Engineering-PWE,Hispanic,Male,48776 801 | Houston Police Department-HPD,White,Male,91181 802 | Houston Fire Department (HFD),Hispanic,Male,89590 803 | Houston Fire Department (HFD),White,Male,70181 804 | Public Works & Engineering-PWE,White,Male,78076 805 | Houston Fire Department (HFD),White,Male,70181 806 | Houston Fire Department (HFD),Hispanic,Male,89590 807 | Houston Police Department-HPD,White,Male,81239 808 | Public Works & Engineering-PWE,Black,Male,40186 809 | Public Works & Engineering-PWE,Hispanic,Male,69493 810 | Houston Police Department-HPD,Hispanic,Female,47650 811 | Houston Fire Department (HFD),Hispanic,Male,62540 812 | Parks & Recreation,Black,Female,26125 813 | Houston Fire Department (HFD),White,Male,61921 814 | Houston Police Department-HPD,Black,Female,35318 815 | Public Works & Engineering-PWE,Hispanic,Male,45989 816 | Public Works & Engineering-PWE,Hispanic,Male,70115 817 | Houston Police Department-HPD,White,Female,74961 818 | Public Works & Engineering-PWE,Hispanic,Male,65853 819 | Public Works & Engineering-PWE,White,Female,146141 820 | Houston Police Department-HPD,White,Female,55461 821 | Houston Police Department-HPD,White,Male,55461 822 | Houston Police Department-HPD,Black,Male,45279 823 | Houston Fire Department (HFD),Hispanic,Male,43528 824 | Houston Police Department-HPD,White,Male,55461 825 | Houston Fire Department (HFD),White,Male,63166 826 | Houston Police Department-HPD,White,Male,66614 827 | Houston Fire Department (HFD),Black,Male,40170 828 | Houston Airport System (HAS),Black,Female,80791 829 | Public Works & Engineering-PWE,White,Male,50586 830 | Houston Police Department-HPD,White,Male,66614 831 | Health & Human Services,Black,Female,43909 832 | Houston Police Department-HPD,Asian,Male,43443 833 | Houston Fire Department (HFD),Hispanic,Male,40170 834 | Houston Police Department-HPD,Black,Female,27872 835 | Houston Airport System (HAS),Hispanic,Female,67671 836 | Public Works & Engineering-PWE,Hispanic,Female,45490 837 | Public Works & Engineering-PWE,Asian,Male,102297 838 | Houston Police Department-HPD,Black,Female,66614 839 | Houston Airport System (HAS),Black,Male,37669 840 | Houston Police Department-HPD,White,Female,47650 841 | Houston Airport System (HAS),White,Male,61589 842 | Houston Fire Department (HFD),White,Male,55437 843 | Houston Fire Department (HFD),White,Male,28024 844 | Houston Airport System (HAS),White,Male,103686 845 | Houston Fire Department (HFD),White,Male,28024 846 | Houston Fire Department (HFD),Hispanic,Male,78355 847 | Houston Police Department-HPD,Asian,Male,60347 848 | Houston Police Department-HPD,White,Male,81239 849 | Public Works & Engineering-PWE,Black,Male,49109 850 | Public Works & Engineering-PWE,White,Male,66560 851 | Public Works & Engineering-PWE,Hispanic,Female,42453 852 | Parks & Recreation,Black,Female,35339 853 | Houston Police Department-HPD,White,Male,81239 854 | Health & Human Services,Hispanic,Female,28600 855 | Houston Police Department-HPD,White,Male,81239 856 | Houston Fire Department (HFD),White,Male,62540 857 | Houston Police Department-HPD,Black,Male,60347 858 | Houston Fire Department (HFD),Hispanic,Male,70181 859 | Houston Police Department-HPD,White,Female,104455 860 | Houston Fire Department (HFD),White,Male,57815 861 | Houston Police Department-HPD,Hispanic,Male,55461 862 | Houston Fire Department (HFD),White,Male,48190 863 | Houston Airport System (HAS),Hispanic,Female,26125 864 | Health & Human Services,Black,Female,47382 865 | Houston Police Department-HPD,Black,Female,47278 866 | Houston Fire Department (HFD),Black,Male,45791 867 | Houston Police Department-HPD,White,Male,66614 868 | Houston Fire Department (HFD),White,Male,55437 869 | Houston Police Department-HPD,Hispanic,Male,55461 870 | Houston Fire Department (HFD),Black,Female,34902 871 | Houston Airport System (HAS),Black,Female,33467 872 | Houston Police Department-HPD,Asian,Male,52514 873 | Houston Police Department-HPD,Black,Male,55461 874 | Houston Police Department-HPD,White,Male,55461 875 | Public Works & Engineering-PWE,Hispanic,Female,47008 876 | Public Works & Engineering-PWE,Asian,Male,74097 877 | Houston Fire Department (HFD),White,Male,45791 878 | Houston Fire Department (HFD),Black,Male,52644 879 | Public Works & Engineering-PWE,White,Female,43888 880 | Houston Police Department-HPD,White,Male,45279 881 | Houston Fire Department (HFD),White,Male,40170 882 | Public Works & Engineering-PWE,White,Male,97626 883 | Houston Police Department-HPD,White,Male,42000 884 | Parks & Recreation,White,Male,36920 885 | Houston Police Department-HPD,Black,Female,41704 886 | Houston Fire Department (HFD),White,Male,78355 887 | Public Works & Engineering-PWE,White,Female,57842 888 | Houston Airport System (HAS),Black,Male,45698 889 | Houston Fire Department (HFD),Black,Male,62540 890 | Houston Police Department-HPD,White,Female,45279 891 | Public Works & Engineering-PWE,Black,Female,71680 892 | Public Works & Engineering-PWE,White,Female,35027 893 | Houston Fire Department (HFD),White,Male,63166 894 | Public Works & Engineering-PWE,Asian,Male,124861 895 | Houston Police Department-HPD,Black,Male,66614 896 | Houston Fire Department (HFD),Black,Female,36400 897 | Public Works & Engineering-PWE,Black,Male,49587 898 | Health & Human Services,Hispanic,Male,33654 899 | Houston Police Department-HPD,Hispanic,Female,35318 900 | Houston Police Department-HPD,Black,Male,66614 901 | Public Works & Engineering-PWE,Black,Male,38688 902 | Public Works & Engineering-PWE,White,Female,76085 903 | Public Works & Engineering-PWE,Hispanic,Male,32864 904 | Houston Police Department-HPD,White,Male,66614 905 | Houston Fire Department (HFD),White,Male,66523 906 | Houston Police Department-HPD,White,Male,60347 907 | Houston Fire Department (HFD),Black,Female,78355 908 | Houston Fire Department (HFD),Hispanic,Male,40170 909 | Houston Police Department-HPD,White,Female,60347 910 | Houston Police Department-HPD,White,Male,91181 911 | Houston Police Department-HPD,Black,Male,30618 912 | Houston Fire Department (HFD),Black,Male,63166 913 | Public Works & Engineering-PWE,Hispanic,Female,30493 914 | Houston Fire Department (HFD),Hispanic,Male,48190 915 | Houston Police Department-HPD,Asian,Male,66614 916 | Public Works & Engineering-PWE,Black,Female,92028 917 | Public Works & Engineering-PWE,Black,Male,44429 918 | Parks & Recreation,Hispanic,Male,72280 919 | Public Works & Engineering-PWE,Black,Male,40227 920 | Houston Police Department-HPD,White,Male,81239 921 | Public Works & Engineering-PWE,White,Male,39478 922 | Houston Fire Department (HFD),Black,Male,55437 923 | Houston Police Department-HPD,Asian,Male,42000 924 | Public Works & Engineering-PWE,Asian,Female,80433 925 | Houston Police Department-HPD,Black,Female,52333 926 | Houston Police Department-HPD,White,Male,81239 927 | Health & Human Services,Black,Male,41870 928 | Houston Fire Department (HFD),White,Male,62540 929 | Houston Police Department-HPD,White,Male,66614 930 | Houston Police Department-HPD,Asian,Female,55378 931 | Public Works & Engineering-PWE,Black,Female,30347 932 | Houston Airport System (HAS),Black,Female,26125 933 | Houston Police Department-HPD,Hispanic,Male,55461 934 | Public Works & Engineering-PWE,White,Female,62400 935 | Houston Police Department-HPD,Hispanic,Male,77076 936 | Houston Police Department-HPD,White,Male,65416 937 | Houston Fire Department (HFD),White,Male,51194 938 | Houston Fire Department (HFD),Hispanic,Male,62540 939 | Public Works & Engineering-PWE,Black,Male,31450 940 | Houston Police Department-HPD,Black,Female,55461 941 | Houston Fire Department (HFD),Hispanic,Male,43528 942 | Houston Fire Department (HFD),Hispanic,Male,62540 943 | Public Works & Engineering-PWE,White,Female,178331 944 | Houston Fire Department (HFD),White,Male,78355 945 | Public Works & Engineering-PWE,Black,Male,104389 946 | Houston Fire Department (HFD),Hispanic,Male,57815 947 | Houston Airport System (HAS),White,Male,74420 948 | Houston Police Department-HPD,Black,Female,26229 949 | Houston Fire Department (HFD),Hispanic,Male,61921 950 | Houston Fire Department (HFD),White,Male,51194 951 | Health & Human Services,Black,Male,90305 952 | Houston Police Department-HPD,White,Male,55461 953 | Health & Human Services,Black,Female,54509 954 | Houston Police Department-HPD,Hispanic,Female,77076 955 | Houston Fire Department (HFD),Black,Male,66523 956 | Public Works & Engineering-PWE,Black,Male,27352 957 | Public Works & Engineering-PWE,Black,Female,53173 958 | Houston Airport System (HAS),White,Male,54080 959 | Houston Fire Department (HFD),White,Male,62540 960 | Houston Police Department-HPD,White,Male,81239 961 | Houston Fire Department (HFD),White,Male,62540 962 | Houston Police Department-HPD,Hispanic,Female,66614 963 | Public Works & Engineering-PWE,Hispanic,Male,54683 964 | Houston Police Department-HPD,White,Male,66614 965 | Houston Police Department-HPD,Black,Female,61643 966 | Houston Police Department-HPD,Black,Female,66614 967 | Houston Fire Department (HFD),Hispanic,Male,63166 968 | Houston Police Department-HPD,White,Female,42000 969 | Public Works & Engineering-PWE,Black,Male,38251 970 | Public Works & Engineering-PWE,Black,Female,36837 971 | Health & Human Services,Hispanic,Female,30410 972 | Public Works & Engineering-PWE,White,Male,51584 973 | Public Works & Engineering-PWE,White,Male,57450 974 | Public Works & Engineering-PWE,Asian,Male,57200 975 | Public Works & Engineering-PWE,Hispanic,Female,44096 976 | Houston Police Department-HPD,White,Male,60347 977 | Houston Fire Department (HFD),White,Male,89590 978 | Houston Police Department-HPD,White,Male,55461 979 | Public Works & Engineering-PWE,Black,Male,51085 980 | Houston Airport System (HAS),Black,Female,36587 981 | Public Works & Engineering-PWE,Black,Female,33613 982 | Houston Police Department-HPD,White,Male,66614 983 | Houston Police Department-HPD,White,Male,77076 984 | Public Works & Engineering-PWE,Asian,Male,32635 985 | Houston Police Department-HPD,Hispanic,Male,55461 986 | Public Works & Engineering-PWE,Black,Male,45989 987 | Houston Police Department-HPD,White,Male,91181 988 | Houston Police Department-HPD,White,Male,66614 989 | Houston Police Department-HPD,White,Male,47650 990 | Houston Fire Department (HFD),Black,Male,52644 991 | Public Works & Engineering-PWE,Black,Female,37357 992 | Houston Police Department-HPD,Hispanic,Male,50621 993 | Public Works & Engineering-PWE,Black,Male,39021 994 | Houston Police Department-HPD,Black,Male,77076 995 | Public Works & Engineering-PWE,Asian,Male,40810 996 | Public Works & Engineering-PWE,Black,Male,30160 997 | Houston Airport System (HAS),Black,Female,29286 998 | Houston Airport System (HAS),Black,Male,33176 999 | Houston Police Department-HPD,Black,Male,47650 1000 | Houston Police Department-HPD,White,Male,66614 1001 | Houston Police Department-HPD,Black,Female,66614 1002 | Houston Police Department-HPD,Hispanic,Female,50190 1003 | Houston Fire Department (HFD),White,Male,40170 1004 | Houston Police Department-HPD,Hispanic,Male,41059 1005 | Houston Fire Department (HFD),Hispanic,Male,28024 1006 | Houston Police Department-HPD,Black,Male,81239 1007 | Public Works & Engineering-PWE,Black,Female,36629 1008 | Houston Police Department-HPD,Hispanic,Male,50621 1009 | Houston Airport System (HAS),Hispanic,Male,38730 1010 | Houston Airport System (HAS),Black,Female,89416 1011 | Houston Police Department-HPD,Hispanic,Female,36920 1012 | Houston Fire Department (HFD),Black,Male,40170 1013 | Houston Police Department-HPD,Hispanic,Male,60347 1014 | Houston Police Department-HPD,Black,Female,66614 1015 | Public Works & Engineering-PWE,White,Male,60715 1016 | Houston Police Department-HPD,White,Male,60347 1017 | Houston Police Department-HPD,White,Male,55461 1018 | Houston Police Department-HPD,White,Male,66614 1019 | Houston Airport System (HAS),Hispanic,Female,53518 1020 | Houston Fire Department (HFD),White,Male,74270 1021 | Houston Fire Department (HFD),Hispanic,Male,61921 1022 | Public Works & Engineering-PWE,Asian,Male,98748 1023 | Houston Airport System (HAS),Hispanic,Female,73635 1024 | Houston Fire Department (HFD),White,Male,45791 1025 | Public Works & Engineering-PWE,Hispanic,Female,29453 1026 | Houston Police Department-HPD,Asian,Female,50621 1027 | Houston Fire Department (HFD),Hispanic,Male,51194 1028 | Houston Police Department-HPD,White,Male,50621 1029 | Public Works & Engineering-PWE,White,Male,53123 1030 | Houston Fire Department (HFD),White,Male,40170 1031 | Houston Police Department-HPD,White,Male,45279 1032 | Public Works & Engineering-PWE,Black,Female,43867 1033 | Houston Police Department-HPD,Black,Male,42000 1034 | Public Works & Engineering-PWE,Black,Male,35318 1035 | Parks & Recreation,Black,Male,26125 1036 | Houston Fire Department (HFD),White,Male,70181 1037 | Houston Police Department-HPD,Black,Male,66614 1038 | Public Works & Engineering-PWE,Black,Male,34382 1039 | Houston Fire Department (HFD),White,Male,37918 1040 | Houston Fire Department (HFD),Black,Male,66523 1041 | Health & Human Services,Asian,Male,70864 1042 | Public Works & Engineering-PWE,Black,Female,36754 1043 | Houston Fire Department (HFD),White,Male,52644 1044 | Public Works & Engineering-PWE,White,Female,78728 1045 | Health & Human Services,White,Male,60082 1046 | Houston Fire Department (HFD),White,Male,61921 1047 | Houston Fire Department (HFD),White,Male,78355 1048 | Houston Police Department-HPD,Black,Female,55016 1049 | Houston Police Department-HPD,Black,Male,86534 1050 | Public Works & Engineering-PWE,Hispanic,Male,51563 1051 | Houston Police Department-HPD,White,Male,55461 1052 | Public Works & Engineering-PWE,Asian,Male,113930 1053 | Houston Police Department-HPD,Hispanic,Male,47650 1054 | Houston Fire Department (HFD),Hispanic,Male,63166 1055 | Houston Police Department-HPD,Black,Male,66614 1056 | Public Works & Engineering-PWE,Hispanic,Male,67038 1057 | Public Works & Engineering-PWE,Black,Female,33613 1058 | Houston Airport System (HAS),Hispanic,Female,28579 1059 | Houston Police Department-HPD,Hispanic,Male,61643 1060 | Houston Fire Department (HFD),White,Male,61921 1061 | Houston Police Department-HPD,White,Male,66614 1062 | Houston Police Department-HPD,Asian,Male,66614 1063 | Houston Fire Department (HFD),White,Male,40165 1064 | Houston Fire Department (HFD),Hispanic,Male,61921 1065 | Houston Police Department-HPD,Black,Female,66614 1066 | Houston Fire Department (HFD),White,Male,78355 1067 | Houston Police Department-HPD,White,Male,47650 1068 | Houston Police Department-HPD,Black,Male,27414 1069 | Houston Fire Department (HFD),Hispanic,Male,62540 1070 | Houston Police Department-HPD,Hispanic,Male,60347 1071 | Houston Fire Department (HFD),White,Male,63166 1072 | Houston Police Department-HPD,Hispanic,Male,31907 1073 | Houston Police Department-HPD,Black,Female,30618 1074 | Houston Fire Department (HFD),White,Male,48190 1075 | Houston Fire Department (HFD),Black,Male,28024 1076 | Public Works & Engineering-PWE,Hispanic,Female,33488 1077 | Houston Police Department-HPD,White,Male,47650 1078 | Houston Fire Department (HFD),White,Male,28024 1079 | Houston Police Department-HPD,White,Male,81239 1080 | Public Works & Engineering-PWE,Hispanic,Male,50606 1081 | Houston Police Department-HPD,Hispanic,Male,47650 1082 | Houston Fire Department (HFD),White,Male,70181 1083 | Houston Police Department-HPD,Hispanic,Male,47650 1084 | Public Works & Engineering-PWE,White,Female,54122 1085 | Houston Fire Department (HFD),Black,Male,71174 1086 | Public Works & Engineering-PWE,Black,Female,33488 1087 | Public Works & Engineering-PWE,Hispanic,Female,44575 1088 | Houston Police Department-HPD,White,Male,66614 1089 | Houston Police Department-HPD,Black,Female,60347 1090 | Houston Fire Department (HFD),White,Male,66523 1091 | Houston Police Department-HPD,Black,Female,53186 1092 | Houston Fire Department (HFD),White,Male,63166 1093 | Public Works & Engineering-PWE,Black,Male,38418 1094 | Houston Police Department-HPD,Asian,Male,47650 1095 | Houston Fire Department (HFD),Hispanic,Male,45791 1096 | Public Works & Engineering-PWE,White,Male,58816 1097 | Houston Airport System (HAS),Black,Male,33301 1098 | Parks & Recreation,Hispanic,Male,26125 1099 | Houston Fire Department (HFD),Black,Male,62540 1100 | Public Works & Engineering-PWE,Black,Female,30347 1101 | Health & Human Services,Black,Female,103270 1102 | Houston Police Department-HPD,White,Male,66614 1103 | Health & Human Services,Asian,Male,47050 1104 | Houston Fire Department (HFD),White,Male,70181 1105 | Houston Fire Department (HFD),White,Male,66523 1106 | Parks & Recreation,Hispanic,Male,26125 1107 | Houston Airport System (HAS),Asian,Female,26125 1108 | Public Works & Engineering-PWE,Black,Male,27102 1109 | Houston Fire Department (HFD),White,Male,62540 1110 | Houston Police Department-HPD,Black,Male,66614 1111 | Houston Police Department-HPD,Hispanic,Female,61643 1112 | Houston Airport System (HAS),Hispanic,Male,39125 1113 | Houston Airport System (HAS),Black,Male,39125 1114 | Houston Fire Department (HFD),Hispanic,Male,48190 1115 | Public Works & Engineering-PWE,White,Male,43514 1116 | Public Works & Engineering-PWE,Hispanic,Female,35485 1117 | Parks & Recreation,Black,Female,29682 1118 | Houston Police Department-HPD,Black,Female,55461 1119 | Houston Airport System (HAS),Hispanic,Male,73065 1120 | Houston Police Department-HPD,White,Female,66614 1121 | Houston Airport System (HAS),White,Male,49275 1122 | Public Works & Engineering-PWE,White,Male,49691 1123 | Public Works & Engineering-PWE,Black,Female,39208 1124 | Houston Airport System (HAS),White,Male,29286 1125 | Houston Fire Department (HFD),Black,Male,89590 1126 | Houston Police Department-HPD,Black,Male,52514 1127 | Houston Fire Department (HFD),Black,Male,55437 1128 | Parks & Recreation,White,Male,85055 1129 | Houston Police Department-HPD,Hispanic,Male,61643 1130 | Houston Fire Department (HFD),White,Male,62540 1131 | Houston Airport System (HAS),Black,Male,96325 1132 | Public Works & Engineering-PWE,Black,Female,35485 1133 | Houston Police Department-HPD,White,Male,66614 1134 | Health & Human Services,White,Female,33821 1135 | Public Works & Engineering-PWE,Hispanic,Female,60248 1136 | Public Works & Engineering-PWE,Black,Male,33987 1137 | Houston Fire Department (HFD),Hispanic,Female,41995 1138 | Public Works & Engineering-PWE,Hispanic,Male,32240 1139 | Houston Police Department-HPD,White,Male,55461 1140 | Houston Police Department-HPD,Hispanic,Male,55461 1141 | Houston Police Department-HPD,White,Male,60347 1142 | Parks & Recreation,Hispanic,Female,83916 1143 | Houston Airport System (HAS),Black,Female,39125 1144 | Houston Airport System (HAS),Asian,Male,77535 1145 | Houston Fire Department (HFD),White,Female,43014 1146 | Houston Fire Department (HFD),White,Male,70181 1147 | Public Works & Engineering-PWE,Hispanic,Male,36130 1148 | Houston Airport System (HAS),Hispanic,Male,57782 1149 | Houston Police Department-HPD,Asian,Male,55461 1150 | Houston Fire Department (HFD),White,Male,63166 1151 | Public Works & Engineering-PWE,White,Female,75224 1152 | Houston Fire Department (HFD),White,Male,66523 1153 | Houston Police Department-HPD,Hispanic,Male,66614 1154 | Houston Fire Department (HFD),White,Male,89590 1155 | Houston Police Department-HPD,White,Male,55461 1156 | Public Works & Engineering-PWE,Black,Male,65000 1157 | Houston Fire Department (HFD),White,Male,63166 1158 | Houston Police Department-HPD,Hispanic,Male,66614 1159 | Public Works & Engineering-PWE,Black,Female,74384 1160 | Houston Fire Department (HFD),White,Male,99480 1161 | Houston Fire Department (HFD),White,Male,48190 1162 | Houston Police Department-HPD,Black,Male,66614 1163 | Houston Police Department-HPD,White,Male,66614 1164 | Houston Police Department-HPD,Hispanic,Male,60347 1165 | Health & Human Services,Black,Male,54857 1166 | Houston Airport System (HAS),Black,Male,51501 1167 | Houston Fire Department (HFD),Hispanic,Male,43528 1168 | Houston Fire Department (HFD),White,Male,55437 1169 | Houston Police Department-HPD,Black,Female,91181 1170 | Houston Police Department-HPD,Black,Female,61643 1171 | Houston Fire Department (HFD),Black,Male,40170 1172 | Houston Fire Department (HFD),White,Male,89590 1173 | Houston Police Department-HPD,Black,Female,45677 1174 | Houston Police Department-HPD,Black,Female,35318 1175 | Parks & Recreation,Black,Female,40706 1176 | Health & Human Services,Black,Female,67782 1177 | Houston Fire Department (HFD),White,Male,66523 1178 | Houston Police Department-HPD,Black,Female,35318 1179 | Houston Police Department-HPD,White,Male,55461 1180 | Health & Human Services,Asian,Female,51861 1181 | Houston Police Department-HPD,White,Male,28169 1182 | Houston Fire Department (HFD),Black,Male,40170 1183 | Houston Police Department-HPD,Hispanic,Male,61643 1184 | Houston Fire Department (HFD),Hispanic,Male,62540 1185 | Houston Police Department-HPD,White,Male,77076 1186 | Houston Fire Department (HFD),Black,Male,74270 1187 | Houston Police Department-HPD,Hispanic,Female,26229 1188 | Houston Fire Department (HFD),White,Male,28024 1189 | Houston Police Department-HPD,Hispanic,Female,47650 1190 | Houston Police Department-HPD,White,Male,55461 1191 | Health & Human Services,Black,Female,47445 1192 | Houston Police Department-HPD,White,Male,43443 1193 | Houston Police Department-HPD,White,Male,81239 1194 | Houston Police Department-HPD,White,Male,55461 1195 | Houston Airport System (HAS),White,Male,39333 1196 | Health & Human Services,White,Female,36150 1197 | Public Works & Engineering-PWE,Black,Male,51397 1198 | Houston Police Department-HPD,White,Male,55461 1199 | Houston Police Department-HPD,White,Male,55461 1200 | Houston Fire Department (HFD),White,Male,70181 1201 | Houston Airport System (HAS),Hispanic,Female,26125 1202 | Houston Police Department-HPD,Hispanic,Male,81239 1203 | Houston Police Department-HPD,Black,Male,66614 1204 | Health & Human Services,Black,Female,42890 1205 | Houston Police Department-HPD,White,Female,81239 1206 | Houston Police Department-HPD,Black,Male,66614 1207 | Public Works & Engineering-PWE,Black,Male,26125 1208 | Houston Police Department-HPD,Hispanic,Male,28169 1209 | Houston Fire Department (HFD),Hispanic,Male,99480 1210 | Parks & Recreation,Asian,Male,48651 1211 | Houston Police Department-HPD,Asian,Male,55461 1212 | Houston Police Department-HPD,Native American,Male,55461 1213 | Public Works & Engineering-PWE,Hispanic,Male,61564 1214 | Houston Fire Department (HFD),Hispanic,Male,43528 1215 | Houston Police Department-HPD,Asian,Male,61643 1216 | Health & Human Services,Hispanic,Male,58406 1217 | Health & Human Services,Hispanic,Female,47445 1218 | Parks & Recreation,Black,Male,55462 1219 | Houston Fire Department (HFD),White,Male,62540 1220 | Houston Fire Department (HFD),White,Male,43528 1221 | Public Works & Engineering-PWE,Hispanic,Male,41184 1222 | Public Works & Engineering-PWE,Asian,Male,89955 1223 | Parks & Recreation,White,Male,57824 1224 | Health & Human Services,Black,Female,88361 1225 | Houston Police Department-HPD,Black,Male,55461 1226 | Houston Fire Department (HFD),White,Male,66523 1227 | Houston Police Department-HPD,White,Male,66614 1228 | Houston Airport System (HAS),Native American,Female,68299 1229 | Houston Airport System (HAS),White,Male,90832 1230 | Houston Police Department-HPD,Black,Female,66614 1231 | Houston Fire Department (HFD),Black,Male,51194 1232 | Houston Fire Department (HFD),White,Male,62540 1233 | Health & Human Services,Black,Female,30846 1234 | Houston Fire Department (HFD),Black,Male,61226 1235 | Public Works & Engineering-PWE,White,Female,52562 1236 | Houston Airport System (HAS),Black,Male,29286 1237 | Public Works & Engineering-PWE,White,Male,27352 1238 | Public Works & Engineering-PWE,Black,Male,60840 1239 | Houston Fire Department (HFD),Black,Male,63166 1240 | Houston Police Department-HPD,Black,Female,63551 1241 | Houston Fire Department (HFD),White,Male,70181 1242 | Houston Police Department-HPD,Black,Male,60347 1243 | Houston Police Department-HPD,White,Male,81239 1244 | Parks & Recreation,White,Male,46566 1245 | Houston Police Department-HPD,White,Male,66614 1246 | Houston Police Department-HPD,Hispanic,Male,81239 1247 | Public Works & Engineering-PWE,White,Female,66867 1248 | Houston Fire Department (HFD),White,Male,66523 1249 | Houston Fire Department (HFD),Black,Male,78355 1250 | Houston Police Department-HPD,Black,Female,91181 1251 | Houston Fire Department (HFD),Hispanic,Male,63166 1252 | Public Works & Engineering-PWE,Hispanic,Male,32136 1253 | Houston Fire Department (HFD),White,Male,63166 1254 | Houston Police Department-HPD,Hispanic,Male,66614 1255 | Houston Police Department-HPD,Black,Female,35318 1256 | Houston Police Department-HPD,Black,Male,66614 1257 | Houston Police Department-HPD,Black,Male,66614 1258 | Houston Airport System (HAS),Black,Male,45240 1259 | Houston Police Department-HPD,Hispanic,Male,81239 1260 | Houston Fire Department (HFD),Black,Male,57815 1261 | Public Works & Engineering-PWE,Asian,Male,53373 1262 | Houston Fire Department (HFD),Hispanic,Male,43528 1263 | Houston Fire Department (HFD),Hispanic,Male,51194 1264 | Public Works & Engineering-PWE,White,Male,98895 1265 | Houston Police Department-HPD,White,Female,55461 1266 | Houston Police Department-HPD,White,Male,66614 1267 | Houston Police Department-HPD,Hispanic,Male,66614 1268 | Houston Police Department-HPD,White,Male,81239 1269 | Health & Human Services,Black,Female,33675 1270 | Public Works & Engineering-PWE,Black,Female,36629 1271 | Houston Police Department-HPD,Hispanic,Male,47650 1272 | Houston Police Department-HPD,White,Male,61755 1273 | Houston Fire Department (HFD),White,Male,70181 1274 | Houston Police Department-HPD,Hispanic,Male,55461 1275 | Houston Fire Department (HFD),Hispanic,Male,70181 1276 | Health & Human Services,Asian,Male,54288 1277 | Houston Fire Department (HFD),White,Male,62540 1278 | Public Works & Engineering-PWE,White,Male,53123 1279 | Houston Police Department-HPD,White,Male,104455 1280 | Public Works & Engineering-PWE,White,Male,83734 1281 | Houston Airport System (HAS),Hispanic,Male,42099 1282 | Houston Fire Department (HFD),White,Male,40170 1283 | Houston Fire Department (HFD),White,Male,70181 1284 | Parks & Recreation,White,Male,26125 1285 | Houston Fire Department (HFD),White,Male,55437 1286 | Public Works & Engineering-PWE,Black,Male,52832 1287 | Parks & Recreation,Hispanic,Female,54607 1288 | Houston Police Department-HPD,Hispanic,Male,61643 1289 | Public Works & Engineering-PWE,White,Female,53061 1290 | Public Works & Engineering-PWE,Black,Male,45926 1291 | Houston Police Department-HPD,Black,Female,60347 1292 | Houston Police Department-HPD,White,Male,66614 1293 | Houston Police Department-HPD,Black,Male,66614 1294 | Public Works & Engineering-PWE,White,Male,57928 1295 | Houston Police Department-HPD,White,Male,42000 1296 | Houston Fire Department (HFD),White,Male,63166 1297 | Houston Fire Department (HFD),Black,Male,57815 1298 | Houston Police Department-HPD,White,Female,55411 1299 | Houston Police Department-HPD,Black,Male,55461 1300 | Public Works & Engineering-PWE,Black,Male,31450 1301 | Public Works & Engineering-PWE,White,Male,54600 1302 | Public Works & Engineering-PWE,Black,Male,33987 1303 | Houston Police Department-HPD,White,Male,45344 1304 | Public Works & Engineering-PWE,White,Male,51584 1305 | Houston Police Department-HPD,Black,Female,37336 1306 | Houston Police Department-HPD,White,Male,66614 1307 | Houston Fire Department (HFD),White,Male,52644 1308 | Public Works & Engineering-PWE,Black,Female,39270 1309 | Parks & Recreation,Black,Male,38147 1310 | Houston Fire Department (HFD),White,Male,62540 1311 | Houston Fire Department (HFD),White,Male,57815 1312 | Houston Police Department-HPD,Hispanic,Male,60347 1313 | Houston Police Department-HPD,Hispanic,Male,66614 1314 | Houston Fire Department (HFD),White,Male,78355 1315 | Public Works & Engineering-PWE,White,Male,52582 1316 | Houston Fire Department (HFD),Hispanic,Male,52644 1317 | Houston Airport System (HAS),Hispanic,Female,96157 1318 | Public Works & Engineering-PWE,White,Male,91063 1319 | Houston Police Department-HPD,White,Male,55461 1320 | Houston Police Department-HPD,Hispanic,Male,47650 1321 | Health & Human Services,Black,Female,36941 1322 | Public Works & Engineering-PWE,Black,Female,45240 1323 | Houston Airport System (HAS),Black,Female,37752 1324 | Houston Police Department-HPD,Hispanic,Male,81239 1325 | Houston Airport System (HAS),White,Male,103427 1326 | Parks & Recreation,Black,Female,30347 1327 | Public Works & Engineering-PWE,Black,Male,43326 1328 | Public Works & Engineering-PWE,Hispanic,Male,44658 1329 | Houston Fire Department (HFD),Black,Female,33405 1330 | Houston Fire Department (HFD),White,Male,66523 1331 | Public Works & Engineering-PWE,Black,Male,53373 1332 | Public Works & Engineering-PWE,Black,Male,31450 1333 | Houston Fire Department (HFD),White,Male,62540 1334 | Houston Police Department-HPD,Black,Male,81239 1335 | Public Works & Engineering-PWE,Black,Male,34320 1336 | Houston Police Department-HPD,White,Female,77076 1337 | Houston Airport System (HAS),Black,Female,33613 1338 | Houston Police Department-HPD,Hispanic,Female,47650 1339 | Houston Police Department-HPD,Asian,Male,47650 1340 | Public Works & Engineering-PWE,Black,Male,26125 1341 | Parks & Recreation,White,Male,29682 1342 | Houston Police Department-HPD,Hispanic,Male,55461 1343 | Houston Police Department-HPD,Black,Male,37024 1344 | Houston Fire Department (HFD),Hispanic,Male,55437 1345 | Houston Fire Department (HFD),White,Male,63166 1346 | Houston Police Department-HPD,Hispanic,Male,81239 1347 | Public Works & Engineering-PWE,Black,Male,73193 1348 | Houston Fire Department (HFD),Black,Male,51194 1349 | Public Works & Engineering-PWE,Black,Male,30139 1350 | Houston Police Department-HPD,Black,Male,61643 1351 | Houston Fire Department (HFD),White,Male,48190 1352 | Houston Fire Department (HFD),White,Male,89590 1353 | Houston Police Department-HPD,White,Male,66614 1354 | Houston Airport System (HAS),Hispanic,Male,35485 1355 | Houston Fire Department (HFD),Hispanic,Male,40170 1356 | Houston Police Department-HPD,White,Male,66614 1357 | Houston Police Department-HPD,White,Female,66614 1358 | Houston Police Department-HPD,White,Male,66614 1359 | Houston Police Department-HPD,Hispanic,Male,47650 1360 | Houston Fire Department (HFD),Hispanic,Male,74270 1361 | Public Works & Engineering-PWE,Asian,Male,45074 1362 | Public Works & Engineering-PWE,Black,Male,97581 1363 | Houston Fire Department (HFD),Hispanic,Male,52644 1364 | Public Works & Engineering-PWE,Black,Male,36130 1365 | Public Works & Engineering-PWE,Black,Female,29931 1366 | Houston Police Department-HPD,White,Male,81239 1367 | Health & Human Services,Black,Female,36358 1368 | Houston Fire Department (HFD),Black,Male,70181 1369 | Parks & Recreation,Black,Female,26125 1370 | Public Works & Engineering-PWE,Hispanic,Male,31158 1371 | Houston Police Department-HPD,Hispanic,Male,74511 1372 | Houston Police Department-HPD,White,Male,81239 1373 | Houston Fire Department (HFD),Black,Male,70181 1374 | Houston Police Department-HPD,Hispanic,Male,66614 1375 | Houston Police Department-HPD,Asian,Male,55461 1376 | Public Works & Engineering-PWE,Black,Female,29016 1377 | Houston Fire Department (HFD),Hispanic,Male,43528 1378 | Public Works & Engineering-PWE,Black,Male,26125 1379 | Houston Fire Department (HFD),White,Male,63166 1380 | Public Works & Engineering-PWE,Black,Male,49150 1381 | Houston Police Department-HPD,White,Male,55461 1382 | Houston Police Department-HPD,Black,Female,35318 1383 | Houston Police Department-HPD,Black,Female,63165 1384 | Public Works & Engineering-PWE,Black,Female,70717 1385 | Houston Police Department-HPD,Hispanic,Male,47650 1386 | Houston Fire Department (HFD),White,Female,55437 1387 | Health & Human Services,Black,Female,66178 1388 | Houston Fire Department (HFD),Hispanic,Male,70181 1389 | Houston Police Department-HPD,White,Male,66614 1390 | Public Works & Engineering-PWE,Hispanic,Female,52562 1391 | Houston Fire Department (HFD),White,Male,52644 1392 | Houston Police Department-HPD,White,Male,104455 1393 | Houston Police Department-HPD,Black,Male,61643 1394 | Public Works & Engineering-PWE,Black,Female,32178 1395 | Houston Police Department-HPD,White,Male,72332 1396 | Houston Police Department-HPD,White,Male,91181 1397 | Houston Police Department-HPD,Black,Male,55461 1398 | Houston Police Department-HPD,White,Male,55461 1399 | Houston Airport System (HAS),Hispanic,Male,40435 1400 | Houston Fire Department (HFD),White,Male,40170 1401 | Houston Fire Department (HFD),Black,Male,48190 1402 | Houston Fire Department (HFD),White,Male,89590 1403 | Houston Police Department-HPD,White,Male,55461 1404 | Houston Police Department-HPD,Hispanic,Male,61643 1405 | Public Works & Engineering-PWE,Asian,Male,114671 1406 | Public Works & Engineering-PWE,Black,Male,43264 1407 | Houston Police Department-HPD,Hispanic,Male,60347 1408 | Houston Police Department-HPD,White,Male,81239 1409 | Public Works & Engineering-PWE,Black,Male,49608 1410 | Houston Airport System (HAS),Asian,Male,32323 1411 | Houston Fire Department (HFD),White,Male,62540 1412 | Health & Human Services,Black,Female,83466 1413 | Public Works & Engineering-PWE,Black,Female,47008 1414 | Houston Fire Department (HFD),Hispanic,Male,28024 1415 | Houston Police Department-HPD,Hispanic,Male,66614 1416 | Health & Human Services,Hispanic,Female,26125 1417 | Public Works & Engineering-PWE,Asian,Male,47840 1418 | Health & Human Services,Black,Male,55806 1419 | Houston Airport System (HAS),Hispanic,Male,76097 1420 | Health & Human Services,Asian,Female,48672 1421 | Public Works & Engineering-PWE,Hispanic,Male,36629 1422 | Houston Police Department-HPD,Black,Male,43443 1423 | Public Works & Engineering-PWE,Hispanic,Male,28766 1424 | Houston Police Department-HPD,White,Male,81239 1425 | Houston Police Department-HPD,Hispanic,Male,55461 1426 | Public Works & Engineering-PWE,Black,Female,38750 1427 | Houston Fire Department (HFD),Hispanic,Male,66523 1428 | Houston Fire Department (HFD),Black,Male,63166 1429 | Public Works & Engineering-PWE,Black,Female,27352 1430 | Public Works & Engineering-PWE,Hispanic,Male,57200 1431 | Houston Fire Department (HFD),Hispanic,Male,70181 1432 | Houston Fire Department (HFD),Hispanic,Male,55437 1433 | Houston Fire Department (HFD),Hispanic,Male,61226 1434 | Public Works & Engineering-PWE,Black,Male,29557 1435 | Houston Fire Department (HFD),White,Male,78355 1436 | Houston Airport System (HAS),Hispanic,Male,115416 1437 | Houston Police Department-HPD,Black,Female,43722 1438 | Houston Fire Department (HFD),White,Male,61226 1439 | Public Works & Engineering-PWE,Black,Male,39666 1440 | Houston Police Department-HPD,Hispanic,Male,66614 1441 | Houston Airport System (HAS),Black,Male,29453 1442 | Health & Human Services,Black,Female,68432 1443 | Health & Human Services,Black,Male,47050 1444 | Houston Fire Department (HFD),Hispanic,Male,61921 1445 | Houston Police Department-HPD,Black,Male,66614 1446 | Houston Airport System (HAS),Hispanic,Female,32906 1447 | Houston Police Department-HPD,White,Female,30888 1448 | Public Works & Engineering-PWE,Black,Male,31158 1449 | Houston Police Department-HPD,Hispanic,Male,66614 1450 | Public Works & Engineering-PWE,White,Male,53123 1451 | Houston Fire Department (HFD),Black,Male,61226 1452 | Houston Fire Department (HFD),Black,Male,62540 1453 | Parks & Recreation,White,Male,29307 1454 | Health & Human Services,Hispanic,Female,27310 1455 | Public Works & Engineering-PWE,White,Female,83036 1456 | Houston Police Department-HPD,Hispanic,Male,81239 1457 | Houston Fire Department (HFD),Hispanic,Male,63166 1458 | Parks & Recreation,Hispanic,Male,47466 1459 | Houston Police Department-HPD,Black,Female,37211 1460 | Health & Human Services,Native American,Female,58855 1461 | Houston Fire Department (HFD),Black,Female,63166 1462 | Houston Fire Department (HFD),White,Male,61921 1463 | Health & Human Services,Black,Female,38563 1464 | Houston Police Department-HPD,White,Male,81239 1465 | Public Works & Engineering-PWE,Black,Male,83734 1466 | Houston Fire Department (HFD),Hispanic,Male,55437 1467 | Houston Fire Department (HFD),White,Male,48190 1468 | Houston Fire Department (HFD),White,Male,66523 1469 | Houston Airport System (HAS),White,Male,39707 1470 | Houston Airport System (HAS),Black,Male,75462 1471 | Houston Fire Department (HFD),White,Male,55437 1472 | Public Works & Engineering-PWE,Asian,Male,28995 1473 | Houston Police Department-HPD,Black,Male,42000 1474 | Public Works & Engineering-PWE,Hispanic,Male,39666 1475 | Health & Human Services,Black,Female,87222 1476 | Houston Police Department-HPD,Black,Female,47650 1477 | Houston Police Department-HPD,Black,Female,35838 1478 | Houston Airport System (HAS),Black,Male,26125 1479 | Houston Fire Department (HFD),White,Male,66523 1480 | Houston Police Department-HPD,Hispanic,Male,55461 1481 | Houston Police Department-HPD,Black,Male,81239 1482 | Houston Airport System (HAS),Black,Female,26125 1483 | Houston Police Department-HPD,Hispanic,Female,34216 1484 | Houston Fire Department (HFD),White,Male,66523 1485 | Houston Police Department-HPD,Black,Female,33550 1486 | Health & Human Services,Asian,Female,44366 1487 | Houston Police Department-HPD,White,Male,66614 1488 | Houston Police Department-HPD,Black,Female,26250 1489 | Houston Police Department-HPD,White,Male,55461 1490 | Public Works & Engineering-PWE,Black,Male,51584 1491 | Houston Police Department-HPD,Black,Female,66614 1492 | Public Works & Engineering-PWE,Black,Male,41267 1493 | Houston Airport System (HAS),White,Female,128606 1494 | Public Works & Engineering-PWE,Black,Female,93089 1495 | Houston Airport System (HAS),Hispanic,Male,43826 1496 | Public Works & Engineering-PWE,Black,Male,56489 1497 | Parks & Recreation,Hispanic,Female,41330 1498 | Houston Police Department-HPD,Black,Female,47650 1499 | Public Works & Engineering-PWE,White,Female,39666 1500 | Houston Police Department-HPD,White,Male,55461 1501 | Houston Police Department-HPD,Hispanic,Male,62481 1502 | Houston Police Department-HPD,White,Male,55461 1503 | Houston Police Department-HPD,Black,Female,37461 1504 | Public Works & Engineering-PWE,Black,Male,37606 1505 | Public Works & Engineering-PWE,Hispanic,Male,36962 1506 | Houston Police Department-HPD,White,Female,66614 1507 | Houston Fire Department (HFD),Black,Female,52000 1508 | Public Works & Engineering-PWE,Black,Male,43264 1509 | Houston Police Department-HPD,Asian,Male,55461 1510 | Houston Police Department-HPD,Black,Female,47996 1511 | Houston Police Department-HPD,White,Male,78286 1512 | Health & Human Services,White,Female,65338 1513 | Houston Police Department-HPD,Hispanic,Male,66614 1514 | Houston Police Department-HPD,Hispanic,Female,45279 1515 | Houston Fire Department (HFD),White,Male,63166 1516 | Public Works & Engineering-PWE,White,Male,77325 1517 | Houston Police Department-HPD,Black,Male,66614 1518 | Houston Airport System (HAS),Asian,Female,32157 1519 | Houston Police Department-HPD,White,Male,45279 1520 | Houston Police Department-HPD,Black,Female,27914 1521 | Houston Police Department-HPD,Hispanic,Male,66614 1522 | Houston Fire Department (HFD),Black,Male,70181 1523 | Houston Fire Department (HFD),Black,Male,55437 1524 | Houston Fire Department (HFD),White,Male,61921 1525 | Houston Police Department-HPD,White,Male,77076 1526 | Public Works & Engineering-PWE,Black,Male,37211 1527 | Parks & Recreation,Black,Female,30347 1528 | Houston Police Department-HPD,Hispanic,Female,44429 1529 | Houston Airport System (HAS),Hispanic,Male,29286 1530 | Houston Police Department-HPD,White,Male,81239 1531 | Houston Police Department-HPD,Black,Male,104455 1532 | Houston Police Department-HPD,White,Male,43443 1533 | Houston Fire Department (HFD),Black,Male,66523 1534 | Houston Police Department-HPD,White,Male,43443 1535 | Houston Police Department-HPD,Asian,Male,55461 1536 | Houston Fire Department (HFD),Hispanic,Male,51194 1537 | --------------------------------------------------------------------------------