Python数据分析中的面向对象

数据科学界的重点是减少数据收集,清理和组织所涉及的复杂性和时间。本文讨论了如何使用软件工程中的面向对象设计技术来减少编码开销并创建健壮,可重用的数据采集和清理系统。我将概述面向对象的设计,并演示使用这些技术从Python中的Web API获取和清理数据的示例。你可以在Github上找到这篇文章的Jupyter Notebook。

面向对象设计

许多现代软件工程利用面向对象设计(OOD)的原理(也称为面向对象编程(OOP))来创建易于扩展,测试和维护的代码库。顾名思义,这种编程范式集中在根据对象思考代码。对象封装与特定实体相关的数据,属性和方法。使用类定义对象,然后可以将其实例化以创建多个对象,称为类的实例。
我们将使用娱乐信息数据库API(一种用于在美国寻找娱乐机会的JSON REST API)来完成类设计的一些示例。

如果您不熟悉Python中的面向对象设计,那么Python Tutorial有一些很好的入门信息。当我们浏览代码示例时,如果您对Python类设计的细节感兴趣,例如“这是什么”self“参数?”,我将在高阶讨论事情,请查看教程更多信息。

使用

由于我们已经介绍了对象的基本概念,让我们考虑创建一个对象来封装数据源,回想一下对象有方法,属性和数据。。要开始识别数据源类的潜在方法,请考虑与数据源交互时使用的典型活动。一旦我们获取了数据,我们可能希望将其清理并格式化以供其他活动使用 - 可视化,分析或特征转化。从面向对象的角度来看,方法'extract'和'clean'可以涵盖这些活动。数据源对象的属性将包括用于保存我们已提取和清理的数据的构造,以及可能的一些标识信息,例如请求URL和我们传递给它的参数。我们的数据源对象的数据将是URL,参数和包含提取和清理数据的DataFrame的值。这UML类图,名为RidbData。UML图提供了类结构和关系的直观表示。顶部显示了类名,中间包含属性名称,底部包含方法名称。通常,方法将伴随它们各自的参数,但为了简洁起见,这里省略了这些参数。

让我们看一些用于创建此数据源对象的Python代码。我们将逐步介绍以下代码的每个部分:

import pandas as pd
import requests
import json
from pandas.io.json import json_normalize
import config
import numpy as np

class RidbData():
   def __init__(self, name, endpoint, url_params):
      self.df = pd.DataFrame()
      self.endpoint = endpoint
      self.url_params = url_params
      self.name = name

 def clean(self) :
      # by replacing '' with np.NaN we can use dropna to remove rows missing 
      # required data, like lat/longs
      self.df = self.df.replace('', np.nan)

      # normalize column names for lat and long. i.e. can be 
      # FacilityLatitude or RecAreaLatitude
      self.df.columns = self.df.columns.str.replace('.*Latitude', 'Latitude')
      self.df.columns = self.df.columns.str.replace('.*Longitude', 'Longitude')
      self.df = self.df.dropna(subset=['Latitude','Longitude'])

   def extract(self):
      request_url = self.endpoint
      response = requests.get(url=self.endpoint,params=self.url_params)
      data = json.loads(response.text)
      self.df = json_normalize(data['RECDATA'])

要创建对象,我们需要一个构造函数方法init。构造函数可用于设置对象的属性并执行任何初始化例程。RIDB有各种不同的end point,因此我们必须指定在'endpoint'参数中创建RidbData对象时要查询的end point,以及我们需要设置的任何url参数,例如RIDB API密钥中的url_params参数。name属性将帮助我们稍后在处理多个RidbData对象时识别此对象。

在我们的提取方法中,我们将查询end point并将JSON响应加载到DataFrame属性'df'中。我们有一个'clean'方法来插入NaN代替空字符串并删除任何没有纬度/经度值的条目,因为我们对没有位置的设施不感兴趣。

为何对象

objects

让我们退后一步。此时您可能想知道为什么我们编写所有代码而不是简单地创建一个函数:

def get_ridb_data(endpoint,url_params):
   response = requests.get(url = endpoint, params = url_params)
   data = json.loads(response.text)
   df = json_normalize(data['RECDATA'])
   df = df.replace('', np.nan)
   df.columns = df.columns.str.replace('.*Latitude', 'Latitude')
   df.columns = df.columns.str.replace('.*Longitude', 'Longitude')
   df = df.dropna(subset=['Latitude','Longitude'])
   return df

我很高兴你问!
如果所有RIDB端点具有相同的响应和端点配置,那么这样的函数将正常工作。考虑使用面向对象技术的时候是你发现自己编写了许多专门的函数和'if'语句,以便对特殊情况的代码进行小的调整。例如,当我们从设施端点获取数据时,我们想要删除任何没有纬度和经度的数据,而当我们查询媒体端点时,我们决定只捕获图像数据:

def get_ridb_data(endpoint,url_params):
   response = requests.get(url = endpoint, params = url_params)
   data = json.loads(response.text)
   df = json_normalize(data['RECDATA'])
   df = df.replace('', np.nan)
   df.columns = df.columns.str.replace('.*Latitude', 'Latitude')
   df.columns = df.columns.str.replace('.*Longitude', 'Longitude')
   df = df.dropna(subset=['Latitude','Longitude'])
   return df

def get_ridb_facility_media(endpoint, url_params):
   # endpoint = https://ridb.recreation.gov/api/v1/facilities/facilityID/media/  
   response = requests.get(url = endpoint, params = url_params) 
   data = json.loads(response.text)
   df = json_normalize(data['RECDATA'])
   df = df[df['MediaType'] == 'Image']
   return df

请注意,我们在两个函数之间有一些相同的行:

response = requests.get(url = endpoint, params = url_params)
data = json.loads(response.text)
df = json_normalize(data['RECDATA'])

编程中的另一个最佳实践称为DRY原则 - Don’t Repeat Yourself。你可能会说“Pfft!它只有三行代码!“但是如果RIDB将其响应记录名称从“RECDATA”更改为“RECDAT”怎么办?然后,您必须在代码中跟踪“RECDATA”的每个实例并替换它。另外,考虑三行是'get_ridb_facility_media'方法代码的75%。

让我们看一下使用上面创建的RidbData对象查询这两个端点。对于设施端点,我们已经准备好了。只需插入我们的端点,对象方法将处理其余的事情:

facilities = RidbData('facilities_name', 'https://ridb.recreation.gov/api/v1/facilities', dict(apiKey = 'MY_RIDB_API_KEY'))
facilities.extract()
facilities.df.head()

这是运行extract()后数据的快照。请注意,我们有一些空白行将填充NaN:


facility_extract

运行clean()方法并检查DataFrame:

facilities.clean()
facilities.df.head()
clean

将填充NaN,并且FacilityLatitude列现在名为Latitude。
根据端点,RIDB将为纬度和经度提供不同的前缀。
通过将其清除到简单的纬度和经度,我们正在标准化下游分析的数据集。
然后,我们可以创建此对象的其他实例,以获取和清除任何类似端点的数据,例如娱乐区域:

recareas = RidbData('recareas_name','https//ridb.recreation.gov/api/v1/recareas', dict(apiKey='MY_RIDB_API_KEY'))
recareas.extract()
recareas.clean()
recareas.df.head()

看一下上面的recareas数据,我们可以确认列名更改和NaN替换。
现在,如果RIDB API更改其“RECDATA”记录名称,我们只需在一个位置更新代码:RidbData类。以下是我们生成的RidbData类的两个实例。在UML中,类实例用顶部显示“实例名称:类名”,中间部分显示属性值。
我们将解决接下来媒体端点所需的不同“clean()”方法。

扩展类

OOD的原则之一是开放封闭原则(open closed principle):类应该对扩展是开放的,而对修改是封闭的。这意味着一旦一个类完成,测试并验证按预期工作,我们希望将它放在一边。每当你触摸一段代码就会产生新错误的可能性,使用开放/封闭原则我们可以减少错误的可能性,并且还保证一个类可以安全地扩展和使用,因为它在将来不会被更改。

那么我们如何修改现有类的功能呢?在我们的示例中,我们有一个媒体端点,它需要一个不同于我们在RidbData类中编码的方法。我们可以扩展RidbData类并提供一个新的干净方法,同时继承构造函数和提取方法的功能。

class RidbMediaData(RidbData):
        def clean(self):
                self.df = self.df[self.df['MediaType'] == 'Image']

当我们创建新类RidbMediaData时,我们将RidbData类传递给类定义。这表明RidbMediaData是基类RidbData的派生类。这主要是为了告诉你这个结构周围使用的语言;关键点是RidbMediaData继承了RidbData类的方法和属性,因此它不必实现init构造函数方法或提取方法 - 它将从RidbData获取这些实现。您需要在派生类中实现的唯一方法是与基类不同的方法或属性。

RidbMediaData继承了RidbData的所有属性和方法,并提供了一个新的clean()方法。

使用我们新的RidbMediaData类,我们可以获得FacilityID 20006的媒体:

facility200006_media = RidbMediaData('facility10media', 
   'https://ridb.recreation.gov/api/v1/facilities/200006/media', 
   dict(apiKey = 'MY_RIDB_API_KEY'))
facility200006_media.extract()
facility200006_media.clean()
facility200006_media.df

媒体端点返回EntityID,与FacilityID相同。我们为FacilityID 200006检索了一张图片。这很不错,但是如果我们想要获得几个设施的媒体怎么办?我们应该为每个设施创建一个新对象吗?这似乎不是对资源的良好利用。相反,我们还可以在RidbMediaData对象中提供一个新的实现提取函数,以循环访问FacilityID列表:

class RidbMediaData(RidbData):
    def clean(self) :
        self.df = self.df[self.df['MediaType'] == 'Image']
   def extract(self):
        request_url = self.endpoint
        for index, param_set in self.url_params.iterrows():
            facility_id = param_set['facilityID']
            req_url = self.endpoint + str(facility_id) + "/media"

            response = requests.get(url=req_url,params=dict(apiKey=param_set['apiKey']))
            data = json.loads(response.text)

            # append new records to self.df if any exist
            if data['RECDATA']:
                new_entry = json_normalize(data['RECDATA'])
                self.df = self.df.append(new_entry)

要使用extract一次从多个工具获取媒体,我们必须对构造函数参数'endpoint'和'url_params'进行一些更改。对于端点,我们将传递设施端点并附加facilityID和“/ media”以创建每个设施的地址。url_params将成为每个工具的API密钥/ facilityID对的DataFrame:

media_url = 'https://ridb.recreation.gov/api/v1/facilities/'
media_params = pd.DataFrame({
    'apiKey':config.API_KEY,
    'facilityID':[200001, 200002, 200003, 200004, 200005, 200006, 200007, 200008]
    })

ridb_media = RidbMediaData('media', media_url, media_params)
ridb_media.extract()
ridb_media.clean()
ridb_media.df.head()

以上合起来

我们现在有两个对象可用于从RIDB API中提取和清理数据。除了通过继承减少重复代码的好处之外,我们还为所有这些数据源提供了统一的接口。我们可以利用这个来创建一个双线数据提取管道!首先让我们设置我们的对象和端点。

facilities_endpoint = 'https://ridb.recreation.gov/api/v1/facilities/'
recareas_endpoint = 'https://ridb.recreation.gov/api/v1/recareas'
key_dict = dict(apiKey = config.API_KEY)
facilities = RidbData('facilities', facilities_endpoint, key_dict)
recareas = RidbData('recareas', recareas_endpoint, key_dict)
facility_media = RidbMediaData('facilitymedia', facilities_endpoint, media_params) 

ridb_data = [facilities,recareas,facility_media]

现在真的很整洁;因为我们的对象都有相同的接口,我们可以用两行来提取和清理所有这些数据:

list(map(lambda x: x.extract(), ridb_data))
list(map(lambda x: x.clean(), ridb_data))

您现在可以检查ridb_data列表中每个对象的已清理数据,即:

facilities.df.head()
image.png

Give it a go for yourself!

总结

我们已经看到了OOD范例可以为我们提供可扩展,可共享的数据分析代码的一些方法。一些关键的要点:

  • 通过继承,不同的对象可以共享相同的代码,从而降低通过功能更改引入错误的可能性,并使我们能够遵循DRY原则。
  • 通过使用关联方法将数据封装在对象中,我们提供了对数据形状及其经历的操作的隐式保证。这对于在功能开发期间跟踪数据操作尤其重要。
  • 通过OOD原则创建统一的接口可以帮助我们简化下游操作。

当您编写代码时,请留意这些“代码气味”,以确定OOD提供帮助的潜在机会:

  1. 通过轻微调整重复自己以适应数据源之间的差异:
  • 只是不同的URL,数据库连接字符串或文件名?一个参数化的功能可能很适合你。
  • 发现自己在提取代码中写了很多“if”语句?可能是时候重构并考虑使用类
  1. 在垂直堆栈中查找类似的功能
  • 我们查看了用于处理不同数据源的过程,并确定了用于清理和获取数据的类似功能。如果你能在代码中找出这样的相似之处,那么OOD可能有所帮助。

我希望您发现此介绍有助于思考如何组织数据分析代码以提高效率和稳健性。OOD还可用于数据科学工作的其他许多方式,包括使用抽象基类进行接口定义,通过继承编写健壮的Web scraper,以及通过封装功能开发来简化机器学习原型。

阅读原文

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,444评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,421评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,363评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,460评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,502评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,511评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,280评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,736评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,014评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,190评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,848评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,531评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,159评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,411评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,067评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,078评论 2 352

推荐阅读更多精彩内容

  • 燕子安居 清晨,被屋檐下靓丽的身影唤醒神志,几周不见,它们已然衔泥筑成新舍,无需装修,不必除甲醛,它们就安居了。 ...
    悠然mafengxian阅读 250评论 0 4
  • 美帝,别猖狂 你必然被你,蹂躏过的人民 埋葬
    华山柳树阅读 827评论 1 9
  • 下午时分下了一场台风雨,本以为会稍微凉爽点,可是屋里仍然像蒸炉一样。晚饭过后,老公问我说要不要去逛超市,顺便蹭一蹭...
    誉满寰中阅读 295评论 0 0
  • 钝角空间不设防,君来客往自徜徉。 有闲赏字留高韵,无暇观花赠墨香。 或点或评贤士隐,亦歌亦赋大家藏。 以文会友天涯...
    钝角阅读 383评论 2 22
  • 文/陈皓 在这秋高气爽的九月 我手捧一束美丽的鲜花 穿着神圣庄严的军服 迈着虔诚、稳健的脚步 ——向您走来 因为今...
    沂蒙文学阅读 1,463评论 7 2